From d946086d12b6a50a6c9f5a7066ce1525da970fd5 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 18 Feb 2025 10:48:09 +0100 Subject: [PATCH 01/50] improve `buildActive` task --- build.gradle.kts | 1 + stonecutter.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9ecb47ce..11f6c526 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -191,6 +191,7 @@ if (stonecutter.current.isActive) { rootProject.tasks.register("buildActive") { group = "project" dependsOn(tasks.named("build")) + finalizedBy("mergeJars") } } diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 0293f08a..e96a29ea 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -75,7 +75,7 @@ tasks.register("mergeJars") { ?.flatMap { File("$it/build/libs").listFiles() ?.filter { file -> file.isFile && !file.name.endsWith("-sources.jar") && file.name.endsWith(".jar") } - ?: error("Couldn't find any mod jar!") + ?: emptyList() } ?: emptyList() From cce13e28c4dd625cbf4b085327e4bf8f707bf67e Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 18 Feb 2025 14:55:44 +0100 Subject: [PATCH 02/50] Initial iteration of client authentication ref: #324 --- .../automodpack_core/GlobalVariables.java | 5 +- .../skidam/automodpack_core/auth/Secrets.java | 31 +++++++ .../automodpack_core/auth/SecretsStore.java | 80 +++++++++++++++++++ .../skidam/automodpack_core/config/Jsons.java | 10 ++- .../netty/HttpServerHandler.java | 23 ++++++ .../automodpack_loader_core/Preload.java | 8 +- .../client/ModpackUpdater.java | 16 +++- .../client/ModpackUtils.java | 11 ++- .../utils/DownloadManager.java | 29 ++++--- .../networking/content/DataPacket.java | 5 +- .../networking/packet/DataC2SPacket.java | 10 ++- .../networking/packet/HandshakeS2CPacket.java | 28 +++++-- 12 files changed, 224 insertions(+), 32 deletions(-) create mode 100644 core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java diff --git a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java index 3b871caa..21b63157 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java +++ b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java @@ -15,6 +15,7 @@ public class GlobalVariables { public static final Logger LOGGER = LogManager.getLogger("AutoModpack"); public static final String MOD_ID = "automodpack"; + public static final String SECRET_REQUEST_HEADER = "AutoModpack-Secret"; public static Boolean DEBUG = false; public static Boolean preload; public static String MC_VERSION; @@ -37,17 +38,17 @@ public class GlobalVariables { // Switches - optional or required packs, chosen by the player, only one can be installed at a time public final static Path hostContentModpackDir = hostModpackDir.resolve("main"); public static Path hostModpackContentFile = hostModpackDir.resolve("automodpack-content.json"); + public static Path hostSecretsFile = hostModpackDir.resolve("automodpack-secrets.json"); public static Path serverConfigFile = automodpackDir.resolve("automodpack-server.json"); public static Path serverCoreConfigFile = automodpackDir.resolve("automodpack-core.json"); // Client - public static final Path clientConfigFile = automodpackDir.resolve("automodpack-client.json"); + public static final Path clientSecretsFile = automodpackDir.resolve("automodpack-secrets.json"); public static final Path modpacksDir = automodpackDir.resolve("modpacks"); public static final String clientConfigFileOverrideResource = "overrides-automodpack-client.json"; public static String clientConfigOverride; // read from inside a jar file on preload, used instead of clientConfigFile if exists public static Path selectedModpackDir; - public static String selectedModpackLink; } diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java new file mode 100644 index 00000000..7810fba2 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java @@ -0,0 +1,31 @@ +package pl.skidam.automodpack_core.auth; + +import pl.skidam.automodpack_core.GlobalVariables; + +import java.security.SecureRandom; +import java.util.Base64; + +public class Secrets { + public record Secret(String secret, Long timestamp) { } + + public static Secret generateSecret() { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[32]; // 32 bytes = 256 bits + random.nextBytes(bytes); + String secret = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + long timestamp = System.currentTimeMillis() / 1000; + + return new Secret(secret, timestamp); + } + + public static boolean isSecretValid(String secretStr) { + Secret secret = SecretsStore.getHostSecret(secretStr); + if (secret == null) + return false; + + long secretLifetime = GlobalVariables.serverConfig.secretLifetime * 3600; // in seconds + long currentTime = System.currentTimeMillis() / 1000; + + return currentTime - secret.timestamp() < secretLifetime; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java new file mode 100644 index 00000000..ddc64b8c --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java @@ -0,0 +1,80 @@ +package pl.skidam.automodpack_core.auth; + +import pl.skidam.automodpack_core.GlobalVariables; +import pl.skidam.automodpack_core.config.ConfigTools; +import pl.skidam.automodpack_core.config.Jsons; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class SecretsStore { + private static class SecretsCache { + private final ConcurrentMap cache; + private Jsons.SecretsFields db; + private final Path configFile; + + public SecretsCache(Path configFile) { + this.configFile = configFile; + this.cache = new ConcurrentHashMap<>(); + } + + public synchronized void load() { + if (db != null) + return; + db = ConfigTools.load(configFile, Jsons.SecretsFields.class); + if (db != null && db.secrets != null && !db.secrets.isEmpty()) { + cache.putAll(db.secrets); + } + } + + public synchronized void save() { + ConfigTools.save(configFile, db); + } + + public Secrets.Secret get(String key) { + load(); + return cache.get(key); + } + + public void save(String key, Secrets.Secret secret) { + if (key == null || key.isBlank() || secret == null) + return; + load(); + cache.put(key, secret); + if (db == null) { + db = new Jsons.SecretsFields(); + } + db.secrets.put(key, secret); + save(); + } + } + + private static final SecretsCache hostSecrets = new SecretsCache(GlobalVariables.hostSecretsFile); + private static final SecretsCache clientSecrets = new SecretsCache(GlobalVariables.clientSecretsFile); + + public static Secrets.Secret getHostSecret(String secret) { + hostSecrets.load(); + for (var entry : hostSecrets.cache.entrySet()) { + var thisSecret = entry.getValue().secret(); + if (Objects.equals(thisSecret, secret)) { + return entry.getValue(); + } + } + + return null; + } + + public static void saveHostSecret(String uuid, Secrets.Secret secret) { + hostSecrets.save(uuid, secret); + } + + public static Secrets.Secret getClientSecret(String modpack) { + return clientSecrets.get(modpack); + } + + public static void saveClientSecret(String modpack, Secrets.Secret secret) { + clientSecrets.save(modpack, secret); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java b/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java index b61b2292..705aa646 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java @@ -1,11 +1,13 @@ package pl.skidam.automodpack_core.config; +import pl.skidam.automodpack_core.auth.Secrets; + +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; - public class Jsons { public static class ClientConfigFields { @@ -39,6 +41,7 @@ public static class ServerConfigFields { public boolean updateIpsOnEveryStart = false; public int hostPort = -1; public boolean reverseProxy = false; + public long secretLifetime = 336; // 336 hours = 14 days public boolean selfUpdater = false; public List acceptedLoaders; } @@ -55,6 +58,11 @@ public static class WorkaroundFields { public Set workaroundMods; } + + public static class SecretsFields { + public Map secrets = new HashMap<>(); + } + public static class ModpackContentFields { public String modpackName = ""; public String automodpackVersion = ""; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java index 99949d38..e4eaea20 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java @@ -7,6 +7,7 @@ import io.netty.buffer.Unpooled; import io.netty.channel.*; import pl.skidam.automodpack_core.GlobalVariables; +import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.modpack.ModpackContent; import java.io.FileNotFoundException; @@ -35,6 +36,17 @@ public void channelRead(ChannelHandlerContext context, ByteBuf buf, Object msg) return; } + final String secret = parseSecret(request); + if (secret == null || secret.isBlank()) { + dropConnection(context, msg); + return; + } + + if (!Secrets.isSecretValid(secret)) { + dropConnection(context, msg); + return; + } + final String requestUri = parseRequestUri(request); if (requestUri == null) { dropConnection(context, msg); @@ -218,6 +230,17 @@ private String parseRequestUri(String request) { } } + private String parseSecret(String request) { + final String[] requestLines = request.split("\r\n"); + for (String line : requestLines) { + if (line.contains(SECRET_REQUEST_HEADER)) { + return line.replace(SECRET_REQUEST_HEADER + ": ", "").trim(); + } + } + + return null; + } + public List parseBodyStrings(String requestPacket) { List stringList = new ArrayList<>(); if (!requestPacket.contains("[")) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java index 684ff57d..4532bb5d 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java @@ -1,5 +1,7 @@ package pl.skidam.automodpack_loader_core; +import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.utils.CustomFileUtils; @@ -58,7 +60,9 @@ private void updateAll() { return; } - var optionalLatestModpackContent = ModpackUtils.requestServerModpackContent(selectedModpackLink); + Secrets.Secret secret = SecretsStore.getClientSecret(clientConfig.selectedModpack); + + var optionalLatestModpackContent = ModpackUtils.requestServerModpackContent(selectedModpackLink, secret); var latestModpackContent = ConfigTools.loadModpackContent(selectedModpackDir.resolve(hostModpackContentFile.getFileName())); // Use the latest modpack content if available @@ -75,7 +79,7 @@ private void updateAll() { } // Update modpack - new ModpackUpdater().prepareUpdate(latestModpackContent, selectedModpackLink, selectedModpackDir); + new ModpackUpdater().prepareUpdate(latestModpackContent, selectedModpackLink, secret, selectedModpackDir); } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 43d91fa1..6bdbe524 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -1,5 +1,6 @@ package pl.skidam.automodpack_loader_core.client; +import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.utils.CustomFileUtils; @@ -32,6 +33,7 @@ public class ModpackUpdater { private final Set newDownloadedFiles = new HashSet<>(); // Only files which did not exist before. Because some files may have the same name/path and be updated. private String modpackLink; + private Secrets.Secret modpackSecret; private Path modpackDir; private Path modpackContentFile; @@ -40,9 +42,10 @@ public String getModpackName() { return serverModpackContent.modpackName; } - public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link, Path modpackPath) { + public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link, Secrets.Secret secret, Path modpackPath) { serverModpackContent = modpackContent; modpackLink = link; + modpackSecret = secret; modpackDir = modpackPath; if (modpackLink == null || modpackLink.isEmpty() || modpackPath.toString().isEmpty()) { @@ -145,6 +148,11 @@ public void CheckAndLoadModpack() throws Exception { // TODO split it into different methods, its too long public void startUpdate() { + if (modpackSecret == null) { + LOGGER.error("Cannot update modpack, secret is null"); + return; + } + new ScreenManager().download(downloadManager, getModpackName()); long start = System.currentTimeMillis(); @@ -245,7 +253,7 @@ public void startUpdate() { DownloadManager.Urls urls = new DownloadManager.Urls(); - urls.addUrl(new DownloadManager.Url().getUrl(modpackLink + serverSHA1)); + urls.addUrl(new DownloadManager.Url().getUrl(modpackLink + serverSHA1).addHeader(SECRET_REQUEST_HEADER, modpackSecret.secret())); if (fetchManager.getFetchDatas().containsKey(item.sha1)) { urls.addAllUrls(new DownloadManager.Url().getUrls(fetchManager.getFetchDatas().get(item.sha1).fetchedData().urls())); @@ -293,7 +301,7 @@ public void startUpdate() { // TODO set client to a waiting for the server to respond screen LOGGER.warn("Trying to refresh the modpack content"); LOGGER.info("Sending hashes to refresh: {}", hashesJson); - var refreshedContentOptional = ModpackUtils.refreshServerModpackContent(modpackLink, hashesJson); + var refreshedContentOptional = ModpackUtils.refreshServerModpackContent(modpackLink, modpackSecret, hashesJson); if (refreshedContentOptional.isEmpty()) { LOGGER.error("Failed to refresh the modpack content"); } else { @@ -321,7 +329,7 @@ public void startUpdate() { Path downloadFile = CustomFileUtils.getPath(modpackDir, fileName); DownloadManager.Urls urls = new DownloadManager.Urls(); - urls.addUrl(new DownloadManager.Url().getUrl(modpackLink + serverSHA1)); + urls.addUrl(new DownloadManager.Url().getUrl(modpackLink + serverSHA1).addHeader(SECRET_REQUEST_HEADER, modpackSecret.secret())); LOGGER.info("Retrying to download {} from {}", fileName, urls); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 6b0f5b62..f8a2cd80 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -3,6 +3,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.utils.CustomFileUtils; @@ -378,7 +379,11 @@ public static Path getModpackPath(String url, String modpackName) { return modpackDir; } - public static Optional requestServerModpackContent(String link) { + public static Optional requestServerModpackContent(String link, Secrets.Secret secret) { + + if (secret == null) + return Optional.empty(); + if (link == null) { throw new IllegalArgumentException("Link is null"); } @@ -388,6 +393,7 @@ public static Optional requestServerModpackContent(S try { connection = (HttpURLConnection) new URL(link).openConnection(); connection.setRequestMethod("GET"); + connection.setRequestProperty(SECRET_REQUEST_HEADER, secret.secret()); return connectionToModpack(connection); } catch (Exception e) { @@ -402,7 +408,7 @@ public static Optional requestServerModpackContent(S } - public static Optional refreshServerModpackContent(String link, String body) { + public static Optional refreshServerModpackContent(String link, Secrets.Secret secret, String body) { // send custom http body request to get modpack content, rest the same as getServerModpackContent if (link == null || body == null) { throw new IllegalArgumentException("Link or body is null"); @@ -413,6 +419,7 @@ public static Optional refreshServerModpackContent(S try { connection = (HttpURLConnection) new URL(link + "refresh").openConnection(); connection.setRequestMethod("POST"); + connection.setRequestProperty(SECRET_REQUEST_HEADER, secret.secret()); return connectionToModpack(connection, body); } catch (Exception e) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index e039b500..8143801d 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -8,7 +8,6 @@ import java.net.*; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Instant; import java.util.*; import java.util.concurrent.*; import java.util.zip.GZIPInputStream; @@ -48,7 +47,7 @@ private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownlo int numberOfIndexes = queuedDownload.urls.numberOfUrls - 1; int urlIndex = Math.min(queuedDownload.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes); - String url = queuedDownload.urls.URLs.get(numberOfIndexes - urlIndex).url; + Url url = queuedDownload.urls.URLs.get(numberOfIndexes - urlIndex); boolean interrupted = false; @@ -122,7 +121,7 @@ private synchronized void downloadNext() { } } - private void downloadFile(String urlString, FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException { + private void downloadFile(Url url, FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException { Path outFile = queuedDownload.file; @@ -144,12 +143,7 @@ private void downloadFile(String urlString, FileInspection.HashPathPair hashPath // Files.createFile(outFile); } - URL url = new URL(urlString); - URLConnection connection = url.openConnection(); - connection.addRequestProperty("Accept-Encoding", "gzip"); - connection.addRequestProperty("User-Agent", "github/skidamek/automodpack/" + AM_VERSION); - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); + URLConnection connection = getUrlConnection(url); try (OutputStream outputStream = new FileOutputStream(outFile.toFile()); InputStream rawInputStream = new BufferedInputStream(connection.getInputStream(), BUFFER_SIZE); @@ -170,6 +164,19 @@ private void downloadFile(String urlString, FileInspection.HashPathPair hashPath } } + private URLConnection getUrlConnection(Url url) throws IOException { + URL connectionUrl = new URL(url.url); + URLConnection connection = connectionUrl.openConnection(); + for (Map.Entry header : url.headers.entrySet()) { + connection.addRequestProperty(header.getKey(), header.getValue()); + } + connection.addRequestProperty("Accept-Encoding", "gzip"); + connection.addRequestProperty("User-Agent", "github/skidamek/automodpack/" + AM_VERSION); + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + return connection; + } + public void joinAll() throws InterruptedException { semaphore.acquire(addedToQueue); @@ -248,8 +255,9 @@ public List getUrls(List urls) { return urlList; } - public void addHeader(String headerName, String header) { + public Url addHeader(String headerName, String header) { headers.put(headerName, header); + return this; } } @@ -299,7 +307,6 @@ public QueuedDownload(Path file, Urls urls, int attempts, Runnable successCallba public static class DownloadData { public CompletableFuture future; public Path file; - public final Instant startTime = Instant.now(); DownloadData(CompletableFuture future, Path file) { this.future = future; diff --git a/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java b/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java index ddc4d47b..43ed33a5 100644 --- a/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java @@ -1,15 +1,18 @@ package pl.skidam.automodpack.networking.content; import com.google.gson.Gson; +import pl.skidam.automodpack_core.auth.Secrets; public class DataPacket { public String link; public String modpackName; + public Secrets.Secret secret; public boolean modRequired; - public DataPacket(String link, String modpackName, boolean modRequired) { + public DataPacket(String link, String modpackName, Secrets.Secret secret, boolean modRequired) { this.link = link; this.modpackName = modpackName; + this.secret = secret; this.modRequired = modRequired; } diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index ead1627d..42cd6d6b 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -7,6 +7,8 @@ import pl.skidam.automodpack.mixin.core.ClientConnectionAccessor; import pl.skidam.automodpack.mixin.core.ClientLoginNetworkHandlerAccessor; import pl.skidam.automodpack.networking.content.DataPacket; +import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_loader_core.ReLauncher; import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.client.ModpackUtils; @@ -57,16 +59,20 @@ public static CompletableFuture receive(MinecraftClient minecraft Path modpackDir = ModpackUtils.getModpackPath(link, dataPacket.modpackName); boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, link, Set.of()); + // save secret + Secrets.Secret secret = dataPacket.secret; + SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + Boolean needsDisconnecting = null; - var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(link); + var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(link, secret); if (optionalServerModpackContent.isPresent()) { boolean update = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); if (update) { disconnectImmediately(handler); - new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), link, modpackDir); + new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), link, secret, modpackDir); needsDisconnecting = true; } else if (selectedModpackChanged) { disconnectImmediately(handler); diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index b2b2d82a..ef82c07f 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -15,7 +15,8 @@ import pl.skidam.automodpack.networking.content.HandshakePacket; import pl.skidam.automodpack.networking.PacketSender; import pl.skidam.automodpack.networking.server.ServerLoginNetworking; -import pl.skidam.automodpack_loader_core.loader.LoaderManager; +import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.utils.Ip; import static pl.skidam.automodpack.networking.ModPackets.DATA; @@ -29,6 +30,14 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han GameProfile profile = ((ServerLoginNetworkHandlerAccessor) handler).getGameProfile(); String playerName = profile.getName(); + if (profile.getId() == null){ + LOGGER.error("Player {} doesn't have UUID: {}", playerName, profile.getId()); + } + + if (server.getPlayerManager().checkCanJoin(connection.getAddress(), profile) != null) { + return; // ignore it + } + // TODO: send this packet only if player can join (isnt banned, is whitelisted, etc.) // at the moment it's not possible because of // 'Cannot invoke "java.util.UUID.toString()" because the return value of "com.mojang.authlib.GameProfile.getId()" is null' @@ -50,12 +59,12 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han } } else { Common.players.put(playerName, true); - loginSynchronizer.waitFor(server.submit(() -> handleHandshake(connection, playerName, server.getServerPort(), buf, sender))); + loginSynchronizer.waitFor(server.submit(() -> handleHandshake(connection, profile, server.getServerPort(), buf, sender))); } } - public static void handleHandshake(ClientConnection connection, String playerName, int minecraftServerPort, PacketByteBuf buf, PacketSender packetSender) { - LOGGER.info("{} has installed AutoModpack.", playerName); + public static void handleHandshake(ClientConnection connection, GameProfile profile, int minecraftServerPort, PacketByteBuf buf, PacketSender packetSender) { + LOGGER.info("{} has installed AutoModpack.", profile.getName()); String clientResponse = buf.readString(Short.MAX_VALUE); HandshakePacket clientHandshakePacket = HandshakePacket.fromJson(clientResponse); @@ -102,8 +111,13 @@ public static void handleHandshake(ClientConnection connection, String playerNam linkToSend = serverConfig.hostIp; } + // now we know player is authenticated, packets are encrypted and player is whitelisted + // regenerate unique secret + Secrets.Secret secret = Secrets.generateSecret(); + SecretsStore.saveHostSecret(profile.getId().toString(), secret); + // We send empty string if hostIp/hostLocalIp is not specified in server config. Client will use ip by which it connected to the server in first place. - DataPacket dataPacket = new DataPacket("", serverConfig.modpackName, serverConfig.requireAutoModpackOnClient); + DataPacket dataPacket = new DataPacket("", serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); if (linkToSend != null && !linkToSend.isBlank()) { if (!linkToSend.startsWith("http://") && !linkToSend.startsWith("https://")) { @@ -128,8 +142,8 @@ public static void handleHandshake(ClientConnection connection, String playerNam } } - LOGGER.info("Sending {} modpack link: {}", playerName, linkToSend); - dataPacket = new DataPacket(linkToSend, serverConfig.modpackName, serverConfig.requireAutoModpackOnClient); + LOGGER.info("Sending {} modpack link: {}", profile.getName(), linkToSend); + dataPacket = new DataPacket(linkToSend, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); } String packetContentJson = dataPacket.toJson(); From c9ee4a84eaffd232a7dd17562acd473aef9762d8 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 18 Feb 2025 21:31:54 +0100 Subject: [PATCH 03/50] Ensure player associated to the secret is still whitelisted ref: #324 --- .../automodpack_core/GlobalVariables.java | 6 ++-- .../skidam/automodpack_core/auth/Secrets.java | 26 ++++++++++---- .../automodpack_core/auth/SecretsStore.java | 5 +-- .../loader/GameCallService.java | 7 ++++ .../loader/LoaderManagerService.java | 1 - .../automodpack_core/loader/NullGameCall.java | 10 ++++++ .../netty/HttpServerHandler.java | 5 ++- .../pl/skidam/automodpack/init/Common.java | 5 ++- .../skidam/automodpack/loader/GameCall.java | 35 +++++++++++++++++++ .../mixin/core/MinecraftServerMixin.java | 1 + 10 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java create mode 100644 src/main/java/pl/skidam/automodpack/loader/GameCall.java diff --git a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java index 21b63157..d748b595 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java +++ b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java @@ -3,10 +3,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.automodpack_core.loader.ModpackLoaderService; -import pl.skidam.automodpack_core.loader.NullLoaderManager; -import pl.skidam.automodpack_core.loader.LoaderManagerService; -import pl.skidam.automodpack_core.loader.NullModpackLoader; +import pl.skidam.automodpack_core.loader.*; import pl.skidam.automodpack_core.modpack.Modpack; import pl.skidam.automodpack_core.netty.HttpServer; @@ -24,6 +21,7 @@ public class GlobalVariables { public static String LOADER; public static LoaderManagerService LOADER_MANAGER = new NullLoaderManager(); public static ModpackLoaderService MODPACK_LOADER = new NullModpackLoader(); + public static GameCallService GAME_CALL = new NullGameCall(); public static Path AUTOMODPACK_JAR; public static Path MODS_DIR; public static Modpack modpack; diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java index 7810fba2..c4bd36d3 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java @@ -1,10 +1,11 @@ package pl.skidam.automodpack_core.auth; -import pl.skidam.automodpack_core.GlobalVariables; - +import java.net.SocketAddress; import java.security.SecureRandom; import java.util.Base64; +import static pl.skidam.automodpack_core.GlobalVariables.*; + public class Secrets { public record Secret(String secret, Long timestamp) { } @@ -18,14 +19,27 @@ public static Secret generateSecret() { return new Secret(secret, timestamp); } - public static boolean isSecretValid(String secretStr) { - Secret secret = SecretsStore.getHostSecret(secretStr); + public static boolean isSecretValid(String secretStr, SocketAddress address) { + var playerSecretPair = SecretsStore.getHostSecret(secretStr); + if (playerSecretPair == null) + return false; + + Secret secret = playerSecretPair.getValue(); if (secret == null) return false; - long secretLifetime = GlobalVariables.serverConfig.secretLifetime * 3600; // in seconds + String playerUuid = playerSecretPair.getKey(); + if (!GAME_CALL.canPlayerJoin(address, playerUuid)) // check if associated player is still whitelisted + return false; + + long secretLifetime = serverConfig.secretLifetime * 3600; // in seconds long currentTime = System.currentTimeMillis() / 1000; - return currentTime - secret.timestamp() < secretLifetime; + boolean valid = secret.timestamp() + secretLifetime > currentTime; + + if (!valid) + return false; + + return true; } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java index ddc64b8c..97cd150b 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java @@ -5,6 +5,7 @@ import pl.skidam.automodpack_core.config.Jsons; import java.nio.file.Path; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -54,12 +55,12 @@ public void save(String key, Secrets.Secret secret) { private static final SecretsCache hostSecrets = new SecretsCache(GlobalVariables.hostSecretsFile); private static final SecretsCache clientSecrets = new SecretsCache(GlobalVariables.clientSecretsFile); - public static Secrets.Secret getHostSecret(String secret) { + public static Map.Entry getHostSecret(String secret) { hostSecrets.load(); for (var entry : hostSecrets.cache.entrySet()) { var thisSecret = entry.getValue().secret(); if (Objects.equals(thisSecret, secret)) { - return entry.getValue(); + return entry; } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java b/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java new file mode 100644 index 00000000..34ee0232 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java @@ -0,0 +1,7 @@ +package pl.skidam.automodpack_core.loader; + +import java.net.SocketAddress; + +public interface GameCallService { + boolean canPlayerJoin(SocketAddress address, String id); +} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/loader/LoaderManagerService.java b/core/src/main/java/pl/skidam/automodpack_core/loader/LoaderManagerService.java index 5bf623af..78b2ac6a 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/loader/LoaderManagerService.java +++ b/core/src/main/java/pl/skidam/automodpack_core/loader/LoaderManagerService.java @@ -5,7 +5,6 @@ import java.util.Collection; public interface LoaderManagerService { - enum ModPlatform { FABRIC, QUILT, FORGE, NEOFORGE } enum EnvironmentType { CLIENT, SERVER, UNIVERSAL } diff --git a/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java b/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java new file mode 100644 index 00000000..e9cb9363 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java @@ -0,0 +1,10 @@ +package pl.skidam.automodpack_core.loader; + +import java.net.SocketAddress; + +public class NullGameCall implements GameCallService { + @Override + public boolean canPlayerJoin(SocketAddress address, String id) { + return true; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java index e4eaea20..b8fa214c 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java @@ -13,6 +13,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; +import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -42,7 +43,9 @@ public void channelRead(ChannelHandlerContext context, ByteBuf buf, Object msg) return; } - if (!Secrets.isSecretValid(secret)) { + SocketAddress address = context.channel().remoteAddress(); + + if (!Secrets.isSecretValid(secret, address)) { dropConnection(context, msg); return; } diff --git a/src/main/java/pl/skidam/automodpack/init/Common.java b/src/main/java/pl/skidam/automodpack/init/Common.java index d4992f63..0aecee36 100644 --- a/src/main/java/pl/skidam/automodpack/init/Common.java +++ b/src/main/java/pl/skidam/automodpack/init/Common.java @@ -1,6 +1,8 @@ package pl.skidam.automodpack.init; +import net.minecraft.server.MinecraftServer; import net.minecraft.util.Identifier; +import pl.skidam.automodpack.loader.GameCall; import pl.skidam.automodpack.networking.ModPackets; import pl.skidam.automodpack_core.modpack.Modpack; import pl.skidam.automodpack_core.netty.HttpServer; @@ -13,8 +15,8 @@ public class Common { - // True if has AutoModpack installed public static Map players = new HashMap<>(); + public static MinecraftServer server = null; public static void serverInit() { if (serverConfig.generateModpackOnStart) { @@ -39,6 +41,7 @@ public static void serverInit() { } public static void init() { + GAME_CALL = new GameCall(); httpServer = new HttpServer(); modpack = new Modpack(); } diff --git a/src/main/java/pl/skidam/automodpack/loader/GameCall.java b/src/main/java/pl/skidam/automodpack/loader/GameCall.java new file mode 100644 index 00000000..d88382e0 --- /dev/null +++ b/src/main/java/pl/skidam/automodpack/loader/GameCall.java @@ -0,0 +1,35 @@ +package pl.skidam.automodpack.loader; + +import com.mojang.authlib.GameProfile; +import net.minecraft.util.UserCache; +import pl.skidam.automodpack.init.Common; +import pl.skidam.automodpack_core.loader.GameCallService; + +import java.net.SocketAddress; +import java.util.UUID; + +import static pl.skidam.automodpack_core.GlobalVariables.*; + +public class GameCall implements GameCallService { + + @Override + public boolean canPlayerJoin(SocketAddress address, String id) { + UUID uuid = UUID.fromString(id); + String playerName = "Player"; // mock name, name matters less than UUID anyway + GameProfile profile = new GameProfile(uuid, playerName); + + UserCache userCache = Common.server.getUserCache(); + if (userCache != null) { + profile = userCache.getByUuid(uuid).orElse(profile); + } + + if (Common.server == null) { + LOGGER.error("Server is null?"); + return true; + } + + boolean valid = Common.server.getPlayerManager().checkCanJoin(address, profile) == null; + + return valid; + } +} diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/MinecraftServerMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/MinecraftServerMixin.java index b9e05beb..40663df2 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/MinecraftServerMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/MinecraftServerMixin.java @@ -16,6 +16,7 @@ public class MinecraftServerMixin { /*@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;setFavicon(Lnet/minecraft/server/ServerMetadata;)V", ordinal = 0), method = "runServer") *//*?}*/ private void afterSetupServer(CallbackInfo info) { + Common.server = (MinecraftServer) (Object) this; Common.afterSetupServer(); } From 8f35e98a35ad392316553c660561c0a297b3a0d7 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 18 Feb 2025 21:44:41 +0100 Subject: [PATCH 04/50] Always overwrite modpack content file to the newest even if there's no update. Should fix e.g. downgrading mod to the older version when server is offline --- .../client/ModpackUpdater.java | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 43d91fa1..3d323fae 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -55,12 +55,12 @@ public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link // Handle the case where serverModpackContent is null if (serverModpackContent == null) { - handleOfflineMode(); + CheckAndLoadModpack(); return; } // Prepare for modpack update - this.unModifiedSMC = GSON.toJson(serverModpackContent); + unModifiedSMC = GSON.toJson(serverModpackContent); // Create directories if they don't exist if (!Files.exists(modpackDir)) { @@ -75,6 +75,7 @@ public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link // Check if an update is needed if (!ModpackUtils.isUpdate(serverModpackContent, modpackDir)) { LOGGER.info("Modpack is up to date"); + Files.write(modpackContentFile, unModifiedSMC.getBytes()); CheckAndLoadModpack(); return; } @@ -91,21 +92,6 @@ public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link } } - private void handleOfflineMode() throws Exception { - if (!Files.exists(modpackContentFile)) { - return; - } - - Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(modpackContentFile); - if (modpackContent == null) { - return; - } - - LOGGER.warn("Server is down, or you don't have access to internet, but we still want to load selected modpack"); - CheckAndLoadModpack(); - } - - public void CheckAndLoadModpack() throws Exception { boolean requiresRestart = applyModpack(); From b0884d7a5d53ef3e37378d75b468f54849784d86 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 18 Feb 2025 21:53:22 +0100 Subject: [PATCH 05/50] Print actual url instead of class --- .../skidam/automodpack_loader_core/utils/DownloadManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 8143801d..9b42e605 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -73,7 +73,7 @@ private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownlo // Runs on success failed = false; downloaded++; - LOGGER.info("Successfully downloaded {} from {}", queuedDownload.file.getFileName(), url); + LOGGER.info("Successfully downloaded {} from {}", queuedDownload.file.getFileName(), url.url); queuedDownload.successCallback.run(); semaphore.release(); } From 8a5c5c40d91de22a471bc4b6080f84a53c03d8dc Mon Sep 17 00:00:00 2001 From: skidam Date: Fri, 21 Feb 2025 21:09:36 +0100 Subject: [PATCH 06/50] stuff:tm: --- .../automodpack_core/utils/CustomFileUtils.java | 4 ++++ .../automodpack_loader_core/client/ModpackUtils.java | 11 ++++++----- .../mods/ModpackLoader16.java | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java index a3980afd..1c8acd52 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java @@ -198,6 +198,10 @@ public static void dummyIT(Path file) { } public static String getHash(Path file) { + if (!Files.exists(file)) { + return null; + } + try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), "r")) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 6b0f5b62..9492b5ef 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -139,16 +139,17 @@ public static boolean fixNestedMods(List conflictingNestedMo return false; final List standardModIDs = standardModList.stream().map(FileInspection.Mod::modID).toList(); +// LOGGER.info("standardModIDs: {}", standardModIDs); boolean needsRestart = false; for (FileInspection.Mod mod : conflictingNestedMods) { + // Check mods provides, if theres some mod which is named with the same id as some other mod 'provides' remove the mod which provides that id as well, otherwise loader will crash + if (standardModIDs.stream().anyMatch(mod.providesIDs()::contains)) + continue; + Path modPath = mod.modPath(); Path standardModPath = MODS_DIR.resolve(modPath.getFileName()); - if (!CustomFileUtils.hashCompare(modPath, standardModPath)) { - // Check mods provides, if theres some mod which is named with the same id as some other mod 'provides' remove the mod which provides that id as well, otherwise loader will crash - if (standardModIDs.stream().anyMatch(mod.providesIDs()::contains)) - continue; - + if (!Objects.equals(CustomFileUtils.getHash(standardModPath), mod.hash())) { needsRestart = true; LOGGER.info("Copying nested mod {} to standard mods folder", standardModPath.getFileName()); CustomFileUtils.copyFile(modPath, standardModPath); diff --git a/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java b/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java index b8e305e5..10250dd7 100644 --- a/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java +++ b/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java @@ -132,7 +132,7 @@ public List getModpackNestedConflicts(Path modpackDir) { for (ModCandidateImpl mod : conflictingNestedModsImpl) { String originModId = mod.getParentMods().stream().filter(ModCandidateImpl::isRoot).findFirst().map(ModCandidateImpl::getId).orElse(null); if (originModId == null) { - LOGGER.error("Why would it be null? {} - {}", mod, mod.getOriginPaths()); + LOGGER.error("Why would it be null? {} - {}, parent mods: {}", mod, mod.getOriginPaths(), mod.getParentMods()); } else { originModIds.add(originModId); } From 8cf96b85974ea87bb3499fcc290b03e16e1d2088 Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 23 Feb 2025 15:29:01 +0100 Subject: [PATCH 07/50] Rewrite network stack, ditch plain text HTTP for custom protocol secured with TLS 1.3 ref: #324 #319 #329 --- core/build.gradle.kts | 5 +- .../automodpack_core/GlobalVariables.java | 16 +- .../skidam/automodpack_core/auth/Secrets.java | 26 +- .../automodpack_core/auth/SecretsStore.java | 2 +- .../skidam/automodpack_core/config/Jsons.java | 1 + .../modpack/ModpackContent.java | 8 +- .../automodpack_core/netty/HttpResponse.java | 31 -- .../netty/HttpServerHandler.java | 280 ------------------ .../netty/InterConnector.java | 35 --- .../automodpack_core/netty/NetUtils.java | 137 +++++++++ .../{HttpServer.java => NettyServer.java} | 134 ++++++--- .../netty/client/DownloadClient.java | 170 +++++++++++ .../netty/client/EchoClient.java | 132 +++++++++ .../netty/client/NettyClient.java | 14 + .../netty/handler/FileDownloadHandler.java | 154 ++++++++++ .../netty/handler/ProtocolClientHandler.java | 90 ++++++ .../netty/handler/ProtocolMessageDecoder.java | 65 ++++ .../netty/handler/ProtocolMessageEncoder.java | 51 ++++ .../netty/handler/ProtocolServerHandler.java | 66 +++++ .../netty/handler/ServerMessageHandler.java | 173 +++++++++++ .../netty/handler/ZstdDecoder.java | 39 +++ .../netty/handler/ZstdEncoder.java | 25 ++ .../netty/message/EchoMessage.java | 22 ++ .../netty/message/FileRequestMessage.java | 22 ++ .../netty/message/FileResponseMessage.java | 22 ++ .../netty/message/ProtocolMessage.java | 28 ++ .../netty/message/RefreshRequestMessage.java | 28 ++ .../utils/CustomFileUtils.java | 25 +- .../pl/skidam/automodpack_core/utils/Ip.java | 5 +- .../pl/skidam/automodpack_core/utils/Url.java | 19 -- .../automodpack_loader_core/Preload.java | 14 +- .../automodpack_loader_core/SelfUpdater.java | 3 +- .../client/ModpackUpdater.java | 62 ++-- .../client/ModpackUtils.java | 144 +++++++-- .../utils/DownloadManager.java | 138 +++++---- loader/loader-fabric-core.gradle.kts | 3 + loader/loader-forge.gradle.kts | 3 + .../client/audio/CustomSoundInstance.java | 8 +- .../client/ui/versioned/VersionedScreen.java | 12 +- .../client/ui/widget/ListEntryWidget.java | 12 +- .../pl/skidam/automodpack/init/Common.java | 8 +- .../mixin/core/MusicTrackerMixin.java | 16 +- .../mixin/core/ServerNetworkIoMixin.java | 18 +- .../skidam/automodpack/modpack/Commands.java | 22 +- .../networking/content/DataPacket.java | 8 +- .../networking/packet/DataC2SPacket.java | 39 ++- .../networking/packet/HandshakeS2CPacket.java | 58 ++-- stonecutter.gradle.kts | 2 +- 48 files changed, 1738 insertions(+), 657 deletions(-) delete mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/HttpResponse.java delete mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java delete mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/InterConnector.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java rename core/src/main/java/pl/skidam/automodpack_core/netty/{HttpServer.java => NettyServer.java} (52%) create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java create mode 100644 core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java delete mode 100644 core/src/main/java/pl/skidam/automodpack_core/utils/Url.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 7cb9851e..a9e6c66a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,7 +18,10 @@ repositories { dependencies { implementation("org.apache.logging.log4j:log4j-core:2.20.0") implementation("com.google.code.gson:gson:2.10.1") - implementation("io.netty:netty-all:4.1.111.Final") + implementation("io.netty:netty-all:4.1.118.Final") + implementation("org.bouncycastle:bcprov-jdk18on:1.80") + implementation("org.bouncycastle:bcpkix-jdk18on:1.80") + implementation("com.github.luben:zstd-jni:1.5.7-1") implementation("org.tomlj:tomlj:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2") diff --git a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java index d748b595..cc374ebc 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java +++ b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java @@ -5,7 +5,7 @@ import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.loader.*; import pl.skidam.automodpack_core.modpack.Modpack; -import pl.skidam.automodpack_core.netty.HttpServer; +import pl.skidam.automodpack_core.netty.NettyServer; import java.nio.file.Path; @@ -25,24 +25,28 @@ public class GlobalVariables { public static Path AUTOMODPACK_JAR; public static Path MODS_DIR; public static Modpack modpack; - public static HttpServer httpServer; + public static NettyServer hostServer; public static Jsons.ServerConfigFields serverConfig; public static Jsons.ClientConfigFields clientConfig; public static final Path automodpackDir = Path.of("automodpack"); - public final static Path hostModpackDir = automodpackDir.resolve("host-modpack"); + public static final Path hostModpackDir = automodpackDir.resolve("host-modpack"); // TODO More server modpacks // Main - required // Addons - optional addon packs // Switches - optional or required packs, chosen by the player, only one can be installed at a time - public final static Path hostContentModpackDir = hostModpackDir.resolve("main"); + public static final Path hostContentModpackDir = hostModpackDir.resolve("main"); public static Path hostModpackContentFile = hostModpackDir.resolve("automodpack-content.json"); - public static Path hostSecretsFile = hostModpackDir.resolve("automodpack-secrets.json"); public static Path serverConfigFile = automodpackDir.resolve("automodpack-server.json"); public static Path serverCoreConfigFile = automodpackDir.resolve("automodpack-core.json"); + public static final Path privateDir = automodpackDir.resolve(".private"); + public static final Path serverSecretsFile = privateDir.resolve("automodpack-secrets.json"); + public static final Path serverCertFile = privateDir.resolve("cert.crt"); + public static final Path serverPrivateKeyFile = privateDir.resolve("key.pem"); + // Client public static final Path clientConfigFile = automodpackDir.resolve("automodpack-client.json"); - public static final Path clientSecretsFile = automodpackDir.resolve("automodpack-secrets.json"); + public static final Path clientSecretsFile = privateDir.resolve("automodpack-client-secrets.json"); public static final Path modpacksDir = automodpackDir.resolve("modpacks"); public static final String clientConfigFileOverrideResource = "overrides-automodpack-client.json"; diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java index c4bd36d3..9326f175 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java @@ -1,25 +1,47 @@ package pl.skidam.automodpack_core.auth; +import pl.skidam.automodpack_core.netty.NetUtils; + import java.net.SocketAddress; import java.security.SecureRandom; +import java.security.cert.X509Certificate; import java.util.Base64; import static pl.skidam.automodpack_core.GlobalVariables.*; public class Secrets { - public record Secret(String secret, Long timestamp) { } + public record Secret(String secret, String fingerprint, Long timestamp) { } public static Secret generateSecret() { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[32]; // 32 bytes = 256 bits random.nextBytes(bytes); String secret = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + String fingerprint = generateFingerprint(secret); + if (secret == null || fingerprint == null) + return null; + long timestamp = System.currentTimeMillis() / 1000; - return new Secret(secret, timestamp); + return new Secret(secret, fingerprint, timestamp); + } + + private static String generateFingerprint(String secret) { + try { + X509Certificate cert = hostServer.getCert(); + if (cert == null) + return null; + return NetUtils.getFingerprint(cert, secret); + } catch (Exception e) { + e.printStackTrace(); + return null; + } } public static boolean isSecretValid(String secretStr, SocketAddress address) { + if (!serverConfig.validateSecrets) + return true; + var playerSecretPair = SecretsStore.getHostSecret(secretStr); if (playerSecretPair == null) return false; diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java index 97cd150b..1302216f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java @@ -52,7 +52,7 @@ public void save(String key, Secrets.Secret secret) { } } - private static final SecretsCache hostSecrets = new SecretsCache(GlobalVariables.hostSecretsFile); + private static final SecretsCache hostSecrets = new SecretsCache(GlobalVariables.serverSecretsFile); private static final SecretsCache clientSecrets = new SecretsCache(GlobalVariables.clientSecretsFile); public static Map.Entry getHostSecret(String secret) { diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java b/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java index 705aa646..9d18aeb4 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java @@ -42,6 +42,7 @@ public static class ServerConfigFields { public int hostPort = -1; public boolean reverseProxy = false; public long secretLifetime = 336; // 336 hours = 14 days + public boolean validateSecrets = true; public boolean selfUpdater = false; public List acceptedLoaders; } diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java index c0154465..e70fd24e 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java @@ -83,8 +83,8 @@ public boolean create() { } saveModpackContent(); - if (httpServer != null) { - httpServer.addPaths(pathsMap); + if (hostServer != null) { + hostServer.addPaths(pathsMap); } return true; @@ -116,8 +116,8 @@ public boolean loadPreviousContent() { } } - if (httpServer != null) { - httpServer.addPaths(pathsMap); + if (hostServer != null) { + hostServer.addPaths(pathsMap); } // set all new variables diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpResponse.java b/core/src/main/java/pl/skidam/automodpack_core/netty/HttpResponse.java deleted file mode 100644 index 216aeea1..00000000 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpResponse.java +++ /dev/null @@ -1,31 +0,0 @@ -package pl.skidam.automodpack_core.netty; - -import java.util.HashMap; -import java.util.Map; - -public class HttpResponse { - private final int status; - private final Map headers = new HashMap<>(); - - HttpResponse(int status) { - this.status = status; - } - - void addHeader(String name, String value) { - headers.put(name, value); - } - - String getResponseMessage() { - StringBuilder response = new StringBuilder(); - - response.append("HTTP/1.1 ").append(status).append("\r\n"); - - for (Map.Entry header : headers.entrySet()) { - response.append(header.getKey()).append(": ").append(header.getValue()).append("\r\n"); - } - - response.append("\r\n"); - - return response.toString(); - } -} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java deleted file mode 100644 index b8fa214c..00000000 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServerHandler.java +++ /dev/null @@ -1,280 +0,0 @@ -package pl.skidam.automodpack_core.netty; - -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonSyntaxException; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.*; -import pl.skidam.automodpack_core.GlobalVariables; -import pl.skidam.automodpack_core.auth.Secrets; -import pl.skidam.automodpack_core.modpack.ModpackContent; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.net.SocketAddress; -import java.nio.channels.ClosedChannelException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; - -import static pl.skidam.automodpack_core.GlobalVariables.*; - -public class HttpServerHandler extends ChannelInboundHandlerAdapter { - - @Override - public void channelRead(ChannelHandlerContext context, Object msg) { - ByteBuf buf = (ByteBuf) msg; - channelRead(context, buf, msg); - } - - public void channelRead(ChannelHandlerContext context, ByteBuf buf, Object msg) { - final String request = getRequest(buf); - if (request == null) { - dropConnection(context, msg); - return; - } - - final String secret = parseSecret(request); - if (secret == null || secret.isBlank()) { - dropConnection(context, msg); - return; - } - - SocketAddress address = context.channel().remoteAddress(); - - if (!Secrets.isSecretValid(secret, address)) { - dropConnection(context, msg); - return; - } - - final String requestUri = parseRequestUri(request); - if (requestUri == null) { - dropConnection(context, msg); - return; - } - - var firstContext = context.pipeline().firstContext(); - - if (request.contains(HttpServer.HTTP_REQUEST_GET)) { - sendFile(firstContext, requestUri); - } else if (request.contains(HttpServer.HTTP_REQUEST_REFRESH)) { - // TODO set limit for one ip max 1 request per 5 seconds - refreshModpackFiles(firstContext, request); - } - - buf.release(); - } - - private void dropConnection(ChannelHandlerContext ctx, Object request) { - ctx.pipeline().remove(MOD_ID); - ctx.fireChannelRead(request); - } - - private void refreshModpackFiles(ChannelHandlerContext context, String request) { - List hashes = parseBodyStrings(request); - LOGGER.info("Received refresh request for files of hashes: {}", hashes); - List> creationFutures = new ArrayList<>(); - List modpacks = new ArrayList<>(); - for (String hash : hashes) { - final Optional optionalPath = resolvePath(hash); - if (optionalPath.isEmpty()) continue; - Path path = optionalPath.get(); - ModpackContent modpack = null; - for (var content : GlobalVariables.modpack.modpacks.values()) { - if (!content.pathsMap.getMap().containsKey(hash)) { - continue; - } - - modpack = content; - break; - } - - if (modpack == null) { - continue; - } - - modpacks.add(modpack); - - creationFutures.add(modpack.replaceAsync(path)); - } - - creationFutures.forEach(CompletableFuture::join); - modpacks.forEach(ModpackContent::saveModpackContent); - - LOGGER.info("Sending new modpack-content.json"); - - // Sends new json - sendFile(context, ""); - } - - private void sendFile(ChannelHandlerContext context, String requestUri) { - final Optional optionalPath = resolvePath(requestUri); - - if (optionalPath.isEmpty()) { - sendError(context, 404); - return; - } - - Path path = optionalPath.get(); - - RandomAccessFile raf; - try { - raf = new RandomAccessFile(path.toFile(), "r"); - } catch (FileNotFoundException e) { - sendError(context, 404); - LOGGER.error("Requested file not found!", e); - return; - } catch (Exception e) { - sendError(context, 418); - LOGGER.error("Failed to open the file {}", path, e); - return; - } - - try { - long fileLength = raf.length(); - - HttpResponse response = new HttpResponse(200); - response.addHeader("Content-Type", "application/octet-stream"); - response.addHeader("Content-Length", String.valueOf(fileLength)); - - // Response headers - ByteBuf responseHeadersBuf = Unpooled.copiedBuffer(response.getResponseMessage(), StandardCharsets.UTF_8); - context.pipeline().firstContext().write(responseHeadersBuf); - - // Write the file - DefaultFileRegion fileRegion = new DefaultFileRegion(raf.getChannel(), 0, fileLength); - context.pipeline().firstContext().writeAndFlush(fileRegion, context.newProgressivePromise()) - .addListener(future -> { - try { - Throwable cause = future.cause(); - if (cause != null && !(cause instanceof ClosedChannelException)) { - LOGGER.error("Error writing to channel: {} path: {}", cause, path); - } - } catch (Exception e) { - LOGGER.error("Unexpected error: {}", e.getMessage(), e); - } finally { - try { - raf.close(); // Ensure RandomAccessFile is closed - } catch (IOException e) { - LOGGER.error("Failed to close RandomAccessFile: {} of path: {}", e, path); - } - - // Finally close channel - var firstContext = context.pipeline().firstContext(); - if (firstContext != null) { - firstContext.channel().close(); - } else if (context.channel() != null) { - context.channel().close(); - } - } - }); - } catch (Exception e) { - LOGGER.error("Error during file: {} handling {}", path, e); - try { - raf.close(); // Ensure RandomAccessFile is closed in case of exceptions - } catch (IOException closeEx) { - LOGGER.error("Failed to close RandomAccessFile: {} of path: {}", closeEx, path); - } - } - } - - @Override - public void exceptionCaught(ChannelHandlerContext context, Throwable cause) { - LOGGER.error("Couldn't handle HTTP request!", cause.getCause()); - } - - private void sendError(ChannelHandlerContext context, int status) { - HttpResponse response = new HttpResponse(status); - response.addHeader("Content-Length", String.valueOf(0)); - - ByteBuf responseBuf = Unpooled.copiedBuffer(response.getResponseMessage(), StandardCharsets.UTF_8); - context.pipeline().writeAndFlush(responseBuf).addListener(ChannelFutureListener.CLOSE); - } - - public Optional resolvePath(final String sha1) { - if (sha1.isBlank()) { - return Optional.of(hostModpackContentFile); - } - - return httpServer.getPath(sha1); - } - - public boolean isAutoModpackRequest(ByteBuf buf) { - boolean equals = false; - try { - buf.markReaderIndex(); - byte[] data1 = new byte[HttpServer.HTTP_REQUEST_GET_BASE_BYTES.length]; - buf.readBytes(data1); - buf.resetReaderIndex(); - byte[] data2 = new byte[HttpServer.HTTP_REQUEST_REFRESH_BYTES.length]; - buf.readBytes(data2); - buf.resetReaderIndex(); - equals = Arrays.equals(data1, HttpServer.HTTP_REQUEST_GET_BASE_BYTES) || Arrays.equals(data2, HttpServer.HTTP_REQUEST_REFRESH_BYTES); - } catch (IndexOutOfBoundsException ignored) { - } catch (Exception e) { - LOGGER.error("Couldn't read channel!", e.getCause()); - } - - return equals; - } - - private String parseRequestUri(String request) { - final String[] requestLines = request.split("\r\n"); - final String[] requestFirstLine = requestLines[0].split(" "); - final String requestUrl = requestFirstLine[1]; - - if (requestUrl.contains(HttpServer.HTTP_REQUEST_BASE)) { - return requestUrl.replaceFirst(HttpServer.HTTP_REQUEST_BASE, ""); - } else { - return null; - } - } - - private String parseSecret(String request) { - final String[] requestLines = request.split("\r\n"); - for (String line : requestLines) { - if (line.contains(SECRET_REQUEST_HEADER)) { - return line.replace(SECRET_REQUEST_HEADER + ": ", "").trim(); - } - } - - return null; - } - - public List parseBodyStrings(String requestPacket) { - List stringList = new ArrayList<>(); - if (!requestPacket.contains("[")) { - return stringList; - } - String jsonPart = requestPacket.substring(requestPacket.lastIndexOf("[")).trim(); - try { - JsonArray jsonArray = new Gson().fromJson(jsonPart, JsonArray.class); - for (int i = 0; i < jsonArray.size(); i++) { - stringList.add(jsonArray.get(i).getAsString()); - } - } catch (JsonSyntaxException e) { - LOGGER.error("Couldn't parse JSON from request body!", e.getCause()); - } - - return stringList; - } - - private String getRequest(ByteBuf buf) { - try { - buf.markReaderIndex(); - if (buf.readableBytes() > 4096 || buf.readableBytes() < HttpServer.HTTP_REQUEST_BASE.length()) { - return null; - } - - byte[] data = new byte[buf.readableBytes()]; - buf.readBytes(data); - buf.resetReaderIndex(); - return new String(data); - } catch (Exception e) { - return null; - } - } -} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/InterConnector.java b/core/src/main/java/pl/skidam/automodpack_core/netty/InterConnector.java deleted file mode 100644 index 67fea443..00000000 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/InterConnector.java +++ /dev/null @@ -1,35 +0,0 @@ -package pl.skidam.automodpack_core.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; - -import static pl.skidam.automodpack_core.GlobalVariables.httpServer; - -// We need to check if we want to handle packets internally or not -public class InterConnector extends ChannelInboundHandlerAdapter { - - @Override - public void channelRead(ChannelHandlerContext context, Object msg) { - if (!httpServer.shouldRunInternally()) { - dropConnection(context, msg); - return; - } - - HttpServerHandler handler = new HttpServerHandler(); - ByteBuf buf = (ByteBuf) msg; - buf.markReaderIndex(); - if (handler.isAutoModpackRequest(buf)) { - buf.resetReaderIndex(); - handler.channelRead(context, buf, msg); - } else { - buf.resetReaderIndex(); - dropConnection(context, msg); - } - } - - private void dropConnection(ChannelHandlerContext ctx, Object request) { - ctx.channel().pipeline().remove(this); - ctx.fireChannelRead(request); - } -} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java b/core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java new file mode 100644 index 00000000..8ff7a744 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java @@ -0,0 +1,137 @@ +package pl.skidam.automodpack_core.netty; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import pl.skidam.automodpack_core.utils.CustomFileUtils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.*; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Calendar; +import java.util.Date; + +public class NetUtils { + + public static final int MAGIC_AMMC = 0x414D4D43; + public static final int MAGIC_AMOK = 0x414D4F4B; + + public static final byte ECHO_TYPE = 0x00; + public static final byte FILE_REQUEST_TYPE = 0x01; + public static final byte FILE_RESPONSE_TYPE = 0x02; + public static final byte REFRESH_REQUEST_TYPE = 0x03; + public static final byte END_OF_TRANSMISSION = 0x04; + public static final byte ERROR = 0x05; + + public static String getFingerprint(X509Certificate cert, String secret) throws CertificateEncodingException { + byte[] sharedSecret = secret.getBytes(); + byte[] certificate = cert.getEncoded(); + + Mac hmac; + try { + hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(sharedSecret, "HmacSHA256"); + hmac.init(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); + } + + byte[] fingerprint = hmac.doFinal(certificate); + + return bytesToHex(fingerprint); + } + + public static String bytesToHex(byte[] fingerprint) { + StringBuilder sb = new StringBuilder(); + for (byte b : fingerprint) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + public static KeyPair generateKeyPair() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + public static X509Certificate selfSign(KeyPair keyPair) throws Exception { + Provider bcProvider = new BouncyCastleProvider(); + Security.addProvider(bcProvider); + + long now = System.currentTimeMillis(); + Date startDate = new Date(now); + + X500Name dnName = new X500Name("CN=AutoModpack Self Signed Certificate"); + BigInteger certSerialNumber = new BigInteger(Long.toString(now)); // <-- Using the current timestamp as the certificate serial number + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(startDate); + calendar.add(Calendar.YEAR, 1); // <-- 1 Yr validity, does not matter, we don't validate it anyway + Date endDate = calendar.getTime(); + + String signatureAlgorithm = "SHA256WithRSA"; // <-- Use appropriate signature algorithm based on your keyPair algorithm. + ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate()); + JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dnName, certSerialNumber, startDate, endDate, dnName, keyPair.getPublic()); + + return new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(contentSigner)); + } + + public static void saveCertificate(X509Certificate cert, Path path) throws Exception { + String certPem = "-----BEGIN CERTIFICATE-----\n" + + formatBase64(Base64.getEncoder().encodeToString(cert.getEncoded())) + + "-----END CERTIFICATE-----"; + CustomFileUtils.setupFilePaths(path); + Files.writeString(path, certPem); + } + + public static X509Certificate loadCertificate(Path path) throws Exception { + if (!Files.exists(path)) return null; + String certPem = Files.readString(path); + certPem = certPem.replaceAll("-----BEGIN CERTIFICATE-----", "") + .replaceAll("-----END CERTIFICATE-----", "") + .replaceAll("\n", ""); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(certPem))); + } + + public static void savePrivateKey(PrivateKey key, Path path) throws Exception { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key.getEncoded()); + String keyPem = "-----BEGIN PRIVATE KEY-----\n" + + formatBase64(Base64.getEncoder().encodeToString(keySpec.getEncoded())) + + "-----END PRIVATE KEY-----"; + CustomFileUtils.setupFilePaths(path); + Files.writeString(path, keyPem); + } + + public static PrivateKey loadPrivateKey(Path path) throws Exception { + if (!Files.exists(path)) return null; + String keyPem = Files.readString(path); + keyPem = keyPem.replaceAll("-----BEGIN PRIVATE KEY-----", "") + .replaceAll("-----END PRIVATE KEY-----", "") + .replaceAll("\n", ""); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)); + return KeyFactory.getInstance("RSA").generatePrivate(keySpec); + } + + private static String formatBase64(String base64) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < base64.length(); i += 64) { + sb.append(base64, i, Math.min(i + 64, base64.length())); + sb.append("\n"); + } + return sb.toString(); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServer.java b/core/src/main/java/pl/skidam/automodpack_core/netty/NettyServer.java similarity index 52% rename from core/src/main/java/pl/skidam/automodpack_core/netty/HttpServer.java rename to core/src/main/java/pl/skidam/automodpack_core/netty/NettyServer.java index 9d07c05d..14f75d33 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/HttpServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/NettyServer.java @@ -6,30 +6,36 @@ import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; import pl.skidam.automodpack_core.config.ConfigTools; +import pl.skidam.automodpack_core.netty.handler.ProtocolServerHandler; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; import pl.skidam.automodpack_core.utils.Ip; import pl.skidam.automodpack_core.utils.ObservableMap; import java.net.InetAddress; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.*; import static pl.skidam.automodpack_core.GlobalVariables.*; -public class HttpServer { - public static final String HTTP_REQUEST_BASE = "/automodpack/"; - public static final String HTTP_REQUEST_GET = "GET " + HTTP_REQUEST_BASE; - public static final String HTTP_REQUEST_REFRESH = "POST " + HTTP_REQUEST_BASE + "refresh"; - public static final byte[] HTTP_REQUEST_GET_BASE_BYTES = HTTP_REQUEST_GET.getBytes(StandardCharsets.UTF_8); - public static final byte[] HTTP_REQUEST_REFRESH_BYTES = HTTP_REQUEST_REFRESH.getBytes(StandardCharsets.UTF_8); +// TODO: clean up this class +public class NettyServer { private final Map paths = Collections.synchronizedMap(new HashMap<>()); private ChannelFuture serverChannel; private Boolean shouldHost = false; // needed for stop modpack hosting for minecraft port + private X509Certificate certificate; + private SslContext sslCtx; public void addPaths(ObservableMap paths) { this.paths.putAll(paths.getMap()); @@ -46,48 +52,84 @@ public Optional getPath(String hash) { } public Optional start() { - if (!canStart()) { - return Optional.empty(); - } + try { + X509Certificate cert; + PrivateKey key; + + if (!Files.exists(serverCertFile) || !Files.exists(serverPrivateKeyFile)) { + // Create a self-signed certificate + KeyPair keyPair = NetUtils.generateKeyPair(); + cert = NetUtils.selfSign(keyPair); + key = keyPair.getPrivate(); + + // save it to the file + NetUtils.saveCertificate(cert, serverCertFile); + NetUtils.savePrivateKey(keyPair.getPrivate(), serverPrivateKeyFile); + } else { + cert = NetUtils.loadCertificate(serverCertFile); + key = NetUtils.loadPrivateKey(serverPrivateKeyFile); + } - int port = serverConfig.hostPort; - InetAddress address = new InetSocketAddress(port).getAddress(); - - MultithreadEventLoopGroup eventLoopGroup; - Class socketChannelClass; - if (Epoll.isAvailable()) { - socketChannelClass = EpollServerSocketChannel.class; - eventLoopGroup = new EpollEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Epoll Server IO #%d").setDaemon(true).build()); - } else { - socketChannelClass = NioServerSocketChannel.class; - eventLoopGroup = new NioEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Server IO #%d").setDaemon(true).build()); - } + if (cert == null || key == null) { + throw new IllegalStateException("Failed to load certificate or private key"); + } + + // Shiny TLS 1.3 + certificate = cert; + sslCtx = SslContextBuilder.forServer(key, cert) + .sslProvider(SslProvider.JDK) + .protocols("TLSv1.3") + .ciphers(Arrays.asList( + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256")) + .build(); + + if (!canStart()) { + return Optional.empty(); + } - serverChannel = new ServerBootstrap() - .channel(socketChannelClass) - .childHandler( - new ChannelInitializer<>() { - @Override - protected void initChannel(Channel channel) { - try { - channel.config().setOption(ChannelOption.TCP_NODELAY, true); - } catch (Exception ignored) { - // ignore it - } - - shouldHost = true; - channel.pipeline().addLast(MOD_ID, new HttpServerHandler()); - } + int port = serverConfig.hostPort; + InetAddress address = new InetSocketAddress(port).getAddress(); + + Class socketChannelClass; + MultithreadEventLoopGroup eventLoopGroup; + if (Epoll.isAvailable()) { + socketChannelClass = EpollServerSocketChannel.class; + eventLoopGroup = new EpollEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Epoll Server IO #%d").setDaemon(true).build()); + } else { + socketChannelClass = NioServerSocketChannel.class; + eventLoopGroup = new NioEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Server IO #%d").setDaemon(true).build()); + } + + serverChannel = new ServerBootstrap() + .channel(socketChannelClass) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(MOD_ID, new ProtocolServerHandler(sslCtx)); + shouldHost = true; } - ) - .group(eventLoopGroup) - .localAddress(address, port) - .bind() - .syncUninterruptibly(); + }) + .group(eventLoopGroup) + .localAddress(address, port) + .bind() + .syncUninterruptibly(); + + System.out.println("Netty file server started on port " + port); + } catch (Exception e) { + LOGGER.error("Failed to start Netty server", e); + return Optional.empty(); + } return Optional.ofNullable(serverChannel); } + public boolean shouldHost() { + return shouldHost; + } + // Returns true if stopped successfully public boolean stop() { if (serverChannel == null) { @@ -118,6 +160,14 @@ public boolean shouldRunInternally() { return serverChannel.channel().isOpen(); } + public X509Certificate getCert() { + return certificate; + } + + public SslContext getSslCtx() { + return sslCtx; + } + private boolean canStart() { if (shouldRunInternally()) { return false; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java new file mode 100644 index 00000000..ff629aab --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java @@ -0,0 +1,170 @@ +package pl.skidam.automodpack_core.netty.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.stream.ChunkedWriteHandler; +import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.netty.handler.*; +import pl.skidam.automodpack_core.netty.message.FileRequestMessage; +import pl.skidam.automodpack_core.netty.message.RefreshRequestMessage; + +import javax.net.ssl.SSLException; +import java.net.InetSocketAddress; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; +import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMMC; + +public class DownloadClient extends NettyClient { + private final List channels = new ArrayList<>(); + private final AtomicInteger roundRobinIndex = new AtomicInteger(0); + private final EventLoopGroup group; + private final Bootstrap bootstrap; + private final int poolSize; + private final InetSocketAddress remoteAddress; + private final SslContext sslCtx; + private final Secrets.Secret secret; + private final DownloadClient downloadClient; + private final Semaphore channelLock = new Semaphore(0); + + public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, int poolSize) throws InterruptedException, SSLException { + this.downloadClient = this; + this.remoteAddress = remoteAddress; + this.secret = secret; + this.poolSize = poolSize; + + // Yes, we use the insecure because server uses self-signed cert and we have different way to verify the authenticity + // Via secret and fingerprint, so the encryption strength should be the same, correct me if I'm wrong, thanks + sslCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(SslProvider.JDK) + .protocols("TLSv1.3") + .ciphers(Arrays.asList( + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256")) + .build(); + + group = new NioEventLoopGroup(); + bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(MOD_ID, new ProtocolClientHandler(downloadClient)); + } + }); + + // Initialize channels and wait for the channels in pool. + for (int i = 0; i < poolSize; i++) { + Channel channel = bootstrap.connect(remoteAddress).sync().channel(); + ByteBuf msg = channel.alloc().buffer(4); + msg.writeInt(MAGIC_AMMC); + channel.writeAndFlush(msg); + } + + channelLock.acquire(poolSize); + } + + @Override + public void secureInit(ChannelHandlerContext ctx) { + ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); + ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); + ctx.pipeline().addLast("chunked-write", new ChunkedWriteHandler()); + ctx.pipeline().addLast("protocol-msg-decoder", new ProtocolMessageEncoder()); + } + + @Override + public void addChannel(Channel channel) { + channels.add(channel); + } + + @Override + public void releaseChannel() { + channelLock.release(); + } + + @Override + public Secrets.Secret getSecret() { + return secret; + } + + /** + * Downloads a file by its SHA-1 hash to the specified destination. + * Returns a CompletableFuture that completes when the download finishes. + */ + public CompletableFuture downloadFile(byte[] fileHash, Path destination) { + + // Select a channel via round-robin. + int index = roundRobinIndex.getAndIncrement(); + Channel channel = channels.get(index % channels.size()); + + // Add a new FileDownloadHandler to process this download. + FileDownloadHandler downloadHandler = new FileDownloadHandler(destination); + channel.pipeline().addLast("downloadHandler-" + index, downloadHandler); + + byte[] bsecret = Base64.getUrlDecoder().decode(secret.secret()); + + // Build and send the file request (which carries the secret and file hash). + FileRequestMessage request = new FileRequestMessage((byte) 1, bsecret, fileHash); + channel.writeAndFlush(request); + + // Return the future that will complete when the download finishes. + return downloadHandler.getDownloadFuture(); + } + + /** + * Downloads a file by its SHA-1 hash to the specified destination. + * Returns a CompletableFuture that completes when the download finishes. + */ + public CompletableFuture requestRefresh(byte[][] fileHashes) { + // Select a channel via round-robin. + int index = roundRobinIndex.getAndIncrement(); + Channel channel = channels.get(index % channels.size()); + + // Add a new FileDownloadHandler to process this download. + FileDownloadHandler downloadHandler = new FileDownloadHandler(null); + channel.pipeline().addLast("downloadHandler-" + index, downloadHandler); + + byte[] bsecret = Base64.getUrlDecoder().decode(secret.secret()); + + // Build and send the file request (which carries the secret and file hash). + RefreshRequestMessage request = new RefreshRequestMessage((byte) 1, bsecret, fileHashes); + channel.writeAndFlush(request); + + // Return the future that will complete when the download finishes. + return downloadHandler.getDownloadFuture(); + } + + /** + * Closes all channels in the pool and shuts down the event loop. + */ + public void close() { + for (Channel channel : channels) { + if (channel.isOpen()) { + channel.close(); + } + } + group.shutdownGracefully(); + } + + public SslContext getSslCtx() { + return sslCtx; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java b/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java new file mode 100644 index 00000000..b03d78a3 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java @@ -0,0 +1,132 @@ +package pl.skidam.automodpack_core.netty.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.netty.handler.ProtocolClientHandler; +import pl.skidam.automodpack_core.netty.handler.ProtocolMessageEncoder; +import pl.skidam.automodpack_core.netty.message.EchoMessage; + +import javax.net.ssl.SSLException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMMC; + +public class EchoClient extends NettyClient { + private final List channels = new ArrayList<>(); + private final AtomicInteger roundRobinIndex = new AtomicInteger(0); + private final EventLoopGroup group; + private final Bootstrap bootstrap; + private final InetSocketAddress remoteAddress; + private final SslContext sslCtx; + private final EchoClient echoClient; + private final Semaphore channelLock = new Semaphore(0); + + public EchoClient(InetSocketAddress remoteAddress) throws InterruptedException, SSLException { + this.echoClient = this; + this.remoteAddress = remoteAddress; + + // Yes, we use the insecure because server uses self-signed cert and we have different way to verify the authenticity + // Via secret and fingerprint, so the encryption strength should be the same, correct me if I'm wrong, thanks + sslCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(SslProvider.JDK) + .protocols("TLSv1.3") + .ciphers(Arrays.asList( + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256")) + .build(); + + String[] enabledProtocols = sslCtx.newEngine(ByteBufAllocator.DEFAULT).getEnabledProtocols(); + System.out.println("Enabled protocols: " + String.join(", ", enabledProtocols)); + System.out.println("Secure SslContext created using cipher suites: " + String.join(", ", sslCtx.cipherSuites())); + + group = new NioEventLoopGroup(); + bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(new ProtocolClientHandler(echoClient)); + } + }); + + // Initialize channels and wait for the channels in pool. + Channel channel = bootstrap.connect(remoteAddress).sync().channel(); + ByteBuf msg = channel.alloc().buffer(4); + msg.writeInt(MAGIC_AMMC); + channel.writeAndFlush(msg); + + channelLock.acquire(); + } + + @Override + public void secureInit(ChannelHandlerContext ctx) { + ctx.pipeline().addLast(new ProtocolMessageEncoder()); + } + + @Override + public void addChannel(Channel channel) { + channels.add(channel); + } + + @Override + public void releaseChannel() { + channelLock.release(); + } + + @Override + public Secrets.Secret getSecret() { + return null; + } + + /** + * Downloads a file by its SHA-1 hash to the specified destination. + * Returns a CompletableFuture that completes when the download finishes. + */ + public CompletableFuture sendEcho(byte[] secret, byte[] data) { + // Select a channel via round-robin. + int index = roundRobinIndex.getAndIncrement(); + Channel channel = channels.get(index % channels.size()); + + // Build and send the file request (which carries the secret and file hash). + EchoMessage request = new EchoMessage((byte) 1, secret, data); + channel.writeAndFlush(request); + + // Return the future that will complete when the download finishes. + return null; + } + + /** + * Closes all channels in the pool and shuts down the event loop. + */ + public void close() { + for (Channel channel : channels) { + if (channel.isOpen()) { + channel.close(); + } + } + group.shutdownGracefully(); + } + + public SslContext getSslCtx() { + return sslCtx; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java b/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java new file mode 100644 index 00000000..f72a8c91 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java @@ -0,0 +1,14 @@ +package pl.skidam.automodpack_core.netty.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.ssl.SslContext; +import pl.skidam.automodpack_core.auth.Secrets; + +public abstract class NettyClient { + public abstract SslContext getSslCtx(); + public abstract void secureInit(ChannelHandlerContext ctx); + public abstract void addChannel(Channel channel); + public abstract void releaseChannel(); + public abstract Secrets.Secret getSecret(); +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java new file mode 100644 index 00000000..8b63271c --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java @@ -0,0 +1,154 @@ +package pl.skidam.automodpack_core.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; +import static pl.skidam.automodpack_core.netty.NetUtils.*; + +public class FileDownloadHandler extends ChannelInboundHandlerAdapter { + + private enum State { + WAITING_HEADER, + RECEIVING_FILE, + WAITING_EOT, + COMPLETED, + ERROR + } + + private State state = State.WAITING_HEADER; + private long expectedFileSize; + private long receivedBytes = 0; + private final Path destination; + private FileOutputStream fos; + private List rawFileData; + private final CompletableFuture downloadFuture = new CompletableFuture<>(); + private byte protocolVersion = 0; + + public FileDownloadHandler(Path destination) { + this.destination = destination; + } + + public CompletableFuture getDownloadFuture() { + return downloadFuture; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + if (destination != null) { + fos = new FileOutputStream(destination.toFile()); + } else { + rawFileData = new LinkedList<>(); + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + if (fos != null) { + fos.close(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof ByteBuf buf)) { + LOGGER.warn("Received non-ByteBuf message: {}", msg); + ctx.fireChannelRead(msg); + return; + } + + try { + // State machine to process the response + if (state == State.WAITING_HEADER) { + if (buf.readableBytes() < 10) { + return; + } + + protocolVersion = buf.readByte(); + byte type = buf.readByte(); + if (type == ERROR) { + int errLen = buf.readInt(); + byte[] errBytes = new byte[errLen]; + buf.readBytes(errBytes); + downloadFuture.completeExceptionally(new IOException("Server error: " + new String(errBytes))); + state = State.ERROR; + return; + } + if (type != FILE_RESPONSE_TYPE) { + downloadFuture.completeExceptionally(new IOException("Unexpected message type: " + type)); + state = State.ERROR; + return; + } + expectedFileSize = buf.readLong(); + state = State.RECEIVING_FILE; + } else if (state == State.RECEIVING_FILE) { + // In RECEIVING_FILE state, we write raw file data. + int readable = buf.readableBytes(); + long remaining = expectedFileSize - receivedBytes; + if (readable <= remaining) { + byte[] data = new byte[readable]; + buf.readBytes(data); + if (fos != null) { + fos.write(data); + } else { + rawFileData.add(data); + } + receivedBytes += readable; + } else { + // Read only the bytes that belong to the file. + byte[] data = new byte[(int) remaining]; + buf.readBytes(data); + if (fos != null) { + fos.write(data); + } else { + rawFileData.add(data); + } + receivedBytes += remaining; + state = State.WAITING_EOT; + } + if (receivedBytes == expectedFileSize) { + state = State.WAITING_EOT; + } + } else if (state == State.WAITING_EOT) { + if (buf.readableBytes() < 2) { + return; + } + + byte ver = buf.readByte(); + if (ver != protocolVersion) { + downloadFuture.completeExceptionally(new IOException("Expected protocol version: " + protocolVersion + ", got: " + ver)); + state = State.ERROR; + return; + } + byte type = buf.readByte(); + if (type != END_OF_TRANSMISSION) { + downloadFuture.completeExceptionally(new IOException("Expected EOT, got type: " + type)); + state = State.ERROR; + return; + } + state = State.COMPLETED; + Object result = destination != null ? destination : rawFileData; + downloadFuture.complete(result); + // Remove this handler now that download is complete. + ctx.pipeline().remove(this); + } + } finally { + ReferenceCountUtil.release(buf); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + downloadFuture.completeExceptionally(cause); + ctx.close(); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java new file mode 100644 index 00000000..7f6c45e4 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java @@ -0,0 +1,90 @@ +package pl.skidam.automodpack_core.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.ssl.SslHandler; +import pl.skidam.automodpack_core.netty.NetUtils; +import pl.skidam.automodpack_core.netty.client.NettyClient; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; + +import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMOK; + +public class ProtocolClientHandler extends ByteToMessageDecoder { + + private final NettyClient client; + + public ProtocolClientHandler(NettyClient client) { + this.client = client; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + try { + if (in.readableBytes() < 4) { + return; + } + + int magic = in.getInt(0); + if (magic != MAGIC_AMOK) { + client.releaseChannel(); + } else { + // Consume the packet + in.skipBytes(in.readableBytes()); + + // Set up the pipeline for the protocol + SslHandler sslHandler = client.getSslCtx().newHandler(ctx.alloc()); + ctx.pipeline().addLast("tls", sslHandler); + + // Wait for SSL handshake to complete before sending data + sslHandler.handshakeFuture().addListener(future -> { + if (!future.isSuccess()) { + ctx.close(); + System.err.println("SSL handshake failed"); + return; + } + + try { + Certificate[] certs = sslHandler.engine().getSession().getPeerCertificates(); + if (certs == null || certs.length == 0 || certs.length > 3) { + return; + } + + for (Certificate cert : certs) { + if (cert instanceof X509Certificate x509Cert) { + String fingerprint = NetUtils.getFingerprint(x509Cert, client.getSecret().secret()); + if (fingerprint.equals(client.getSecret().fingerprint())) { + System.out.println("Server certificate verified, fingerprint: " + fingerprint); + client.secureInit(ctx); + break; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + ctx.close(); + } finally { + if (ctx.channel().isOpen()) { + client.addChannel(ctx.channel()); + } + client.releaseChannel(); + } + }); + } + + ctx.pipeline().remove(this); // Always remove this handler after processing + } catch (Exception e) { + e.printStackTrace(); + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java new file mode 100644 index 00000000..e5a38e01 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java @@ -0,0 +1,65 @@ +package pl.skidam.automodpack_core.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import pl.skidam.automodpack_core.netty.NetUtils; +import pl.skidam.automodpack_core.netty.message.EchoMessage; +import pl.skidam.automodpack_core.netty.message.FileRequestMessage; +import pl.skidam.automodpack_core.netty.message.FileResponseMessage; +import pl.skidam.automodpack_core.netty.message.RefreshRequestMessage; + +import java.util.List; + +import static pl.skidam.automodpack_core.netty.NetUtils.*; + +public class ProtocolMessageDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + in.markReaderIndex(); + byte version = in.readByte(); + byte type = in.readByte(); + byte[] secret = new byte[32]; + in.readBytes(secret); + + switch (type) { + case ECHO_TYPE: + int dataLength = in.readInt(); + byte[] data = new byte[dataLength]; + in.readBytes(data); + out.add(new EchoMessage(version, secret, data)); + break; + case FILE_REQUEST_TYPE: + int fileHashLength = in.readInt(); + byte[] fileHash = new byte[fileHashLength]; + in.readBytes(fileHash); + out.add(new FileRequestMessage(version, secret, fileHash)); + break; + case NetUtils.FILE_RESPONSE_TYPE: + int fileLength = in.readInt(); + byte[] fileData = new byte[fileLength]; + in.readBytes(fileData); + out.add(new FileResponseMessage(version, secret, fileData)); + break; + case REFRESH_REQUEST_TYPE: + int fileHashesCount = in.readInt(); + int fileHashesLength = in.readInt(); + byte[][] fileHashesList = new byte[fileHashesCount][]; + for (int i = 0; i < fileHashesCount; i++) { + byte[] fileHashEntry = new byte[fileHashesLength]; + in.readBytes(fileHashEntry); + fileHashesList[i] = fileHashEntry; + } + out.add(new RefreshRequestMessage(version, secret, fileHashesList)); + break; + default: + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java new file mode 100644 index 00000000..6531a86c --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java @@ -0,0 +1,51 @@ +package pl.skidam.automodpack_core.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import pl.skidam.automodpack_core.netty.message.*; + +import static pl.skidam.automodpack_core.netty.NetUtils.*; + +public class ProtocolMessageEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, ProtocolMessage msg, ByteBuf out) throws Exception { + out.writeByte(msg.getVersion()); + out.writeByte(msg.getType()); + out.writeBytes(msg.getSecret()); + + switch (msg.getType()) { + case ECHO_TYPE: + EchoMessage echoMsg = (EchoMessage) msg; + out.writeInt(echoMsg.getDataLength()); + out.writeBytes(echoMsg.getData()); + break; + case FILE_REQUEST_TYPE: + FileRequestMessage fileRequestMessage = (FileRequestMessage) msg; + out.writeInt(fileRequestMessage.getFileHashLength()); + out.writeBytes(fileRequestMessage.getFileHash()); + break; + case FILE_RESPONSE_TYPE: + FileResponseMessage fileResponseMessage = (FileResponseMessage) msg; + out.writeInt(fileResponseMessage.getDataLength()); + out.writeBytes(fileResponseMessage.getData()); + break; + case REFRESH_REQUEST_TYPE: + RefreshRequestMessage refreshRequestMessage = (RefreshRequestMessage) msg; + out.writeInt(refreshRequestMessage.getFileHashesCount()); + out.writeInt(refreshRequestMessage.getFileHashesLength()); + for (byte[] fileHash : refreshRequestMessage.getFileHashesList()) { + out.writeBytes(fileHash); + } + break; + default: + throw new IllegalArgumentException("Unknown message type: " + msg.getType()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java new file mode 100644 index 00000000..4ebfc364 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java @@ -0,0 +1,66 @@ +package pl.skidam.automodpack_core.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.stream.ChunkedWriteHandler; + +import java.util.List; + +import static pl.skidam.automodpack_core.netty.NetUtils.*; + +public class ProtocolServerHandler extends ByteToMessageDecoder { + + private final SslContext sslCtx; + + public ProtocolServerHandler(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + try { + if (in.readableBytes() < 4) { + return; + } + + int magic = in.getInt(0); + if (magic == MAGIC_AMMC) { + // Consume the packet + in.skipBytes(in.readableBytes()); + + // Send acknowledgment + ByteBuf response = ctx.alloc().buffer(4); + response.writeInt(MAGIC_AMOK); + ctx.writeAndFlush(response); + + // Remove all existing handlers from the pipeline + var handlers = ctx.pipeline().toMap(); + handlers.forEach((name, handler) -> ctx.pipeline().remove(handler)); + + // Set up the pipeline for our protocol + ctx.pipeline().addLast("tls", sslCtx.newHandler(ctx.alloc())); + ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); + ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); + ctx.pipeline().addLast("chunked-write", new ChunkedWriteHandler()); + ctx.pipeline().addLast("protocol-msg-decoder", new ProtocolMessageDecoder()); + ctx.pipeline().addLast("msg-handler", new ServerMessageHandler()); + } + + // Always remove this handler after processing if its still there + if (ctx.pipeline().get(this.getClass()) != null) { + ctx.pipeline().remove(this); + } + } catch (Exception e) { + e.printStackTrace(); + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java new file mode 100644 index 00000000..f864b296 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java @@ -0,0 +1,173 @@ +package pl.skidam.automodpack_core.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.stream.ChunkedFile; +import io.netty.util.CharsetUtil; +import pl.skidam.automodpack_core.GlobalVariables; +import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.modpack.ModpackContent; +import pl.skidam.automodpack_core.netty.message.EchoMessage; +import pl.skidam.automodpack_core.netty.message.FileRequestMessage; +import pl.skidam.automodpack_core.netty.message.ProtocolMessage; +import pl.skidam.automodpack_core.netty.message.RefreshRequestMessage; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.SocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static pl.skidam.automodpack_core.GlobalVariables.*; +import static pl.skidam.automodpack_core.netty.NetUtils.*; + +public class ServerMessageHandler extends SimpleChannelInboundHandler { + + private static byte clientProtocolVersion = 0; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) throws Exception { + clientProtocolVersion = msg.getVersion(); + SocketAddress address = ctx.channel().remoteAddress(); + + // Validate the secret + if (!validateSecret(address, msg.getSecret())) { + sendError(ctx, clientProtocolVersion, "Authentication failed"); + return; + } + + switch (msg.getType()) { + case ECHO_TYPE: + EchoMessage echoMsg = (EchoMessage) msg; + ByteBuf echoBuf = Unpooled.buffer(1 + 1 + msg.getSecret().length + echoMsg.getData().length); + echoBuf.writeByte(clientProtocolVersion); + echoBuf.writeByte(ECHO_TYPE); + echoBuf.writeBytes(echoMsg.getSecret()); + echoBuf.writeBytes(echoMsg.getData()); + ctx.writeAndFlush(echoBuf); + ctx.channel().close(); + break; + case FILE_REQUEST_TYPE: + FileRequestMessage fileRequest = (FileRequestMessage) msg; + sendFile(ctx, fileRequest.getFileHash()); + break; + case REFRESH_REQUEST_TYPE: + RefreshRequestMessage refreshRequest = (RefreshRequestMessage) msg; + refreshModpackFiles(ctx, refreshRequest.getFileHashesList()); + break; + default: + sendError(ctx, clientProtocolVersion, "Unknown message type"); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } + + private void refreshModpackFiles(ChannelHandlerContext context, byte[][] FileHashesList) { + List hashes = new ArrayList<>(); + for (byte[] hash : FileHashesList) { + hashes.add(new String(hash)); + } + LOGGER.info("Received refresh request for files of hashes: {}", hashes); + List> creationFutures = new ArrayList<>(); + List modpacks = new ArrayList<>(); + for (String hash : hashes) { + final Optional optionalPath = resolvePath(hash); + if (optionalPath.isEmpty()) continue; + Path path = optionalPath.get(); + ModpackContent modpack = null; + for (var content : GlobalVariables.modpack.modpacks.values()) { + if (!content.pathsMap.getMap().containsKey(hash)) { + continue; + } + + modpack = content; + break; + } + + if (modpack == null) { + continue; + } + + modpacks.add(modpack); + + creationFutures.add(modpack.replaceAsync(path)); + } + + creationFutures.forEach(CompletableFuture::join); + modpacks.forEach(ModpackContent::saveModpackContent); + + LOGGER.info("Sending new modpack-content.json"); + + // Sends new json + sendFile(context, new byte[0]); + } + + + private boolean validateSecret(SocketAddress address, byte[] secret) { + String decodedSecret = Base64.getUrlEncoder().withoutPadding().encodeToString(secret); + return Secrets.isSecretValid(decodedSecret, address); + } + + private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) { + final String sha1 = new String(bsha1, CharsetUtil.UTF_8); + final Optional optionalPath = resolvePath(sha1); + + if (optionalPath.isEmpty() || !Files.exists(optionalPath.get())) { + sendError(ctx, (byte) 1, "File not found"); + return; + } + + final File file = optionalPath.get().toFile(); + + // Send file response header: version, FILE_RESPONSE type, then file size (8 bytes) + ByteBuf responseHeader = Unpooled.buffer(1 + 1 + 8); + responseHeader.writeByte(clientProtocolVersion); + responseHeader.writeByte(FILE_RESPONSE_TYPE); + responseHeader.writeLong(file.length()); + ctx.writeAndFlush(responseHeader); + + // Stream the file using ChunkedFile (chunk size set to 8192 bytes) + try { + RandomAccessFile raf = new RandomAccessFile(file, "r"); + ChunkedFile chunkedFile = new ChunkedFile(raf, 0, raf.length(), 8192); + ctx.writeAndFlush(chunkedFile).addListener((ChannelFutureListener) future -> { + // After the file is sent, send an End-of-Transmission message. + ByteBuf eot = Unpooled.buffer(2); + eot.writeByte((byte) 1); + eot.writeByte(END_OF_TRANSMISSION); + ctx.writeAndFlush(eot); + }); + } catch (IOException e) { + sendError(ctx, (byte) 1, "File transfer error: " + e.getMessage()); + } + } + + public Optional resolvePath(final String sha1) { + if (sha1.isBlank()) { + return Optional.of(hostModpackContentFile); + } + + return hostServer.getPath(sha1); + } + + private void sendError(ChannelHandlerContext ctx, byte version, String errorMessage) { + byte[] errMsgBytes = errorMessage.getBytes(CharsetUtil.UTF_8); + ByteBuf errorBuf = Unpooled.buffer(1 + 1 + 4 + errMsgBytes.length); + errorBuf.writeByte(version); + errorBuf.writeByte(ERROR); + errorBuf.writeInt(errMsgBytes.length); + errorBuf.writeBytes(errMsgBytes); + ctx.writeAndFlush(errorBuf); + ctx.channel().close(); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java new file mode 100644 index 00000000..9f05e100 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java @@ -0,0 +1,39 @@ +package pl.skidam.automodpack_core.netty.handler; + +import com.github.luben.zstd.Zstd; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; + +public class ZstdDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (in.readableBytes() < 8) { + return; + } + + int length = in.readInt(); + int originalLength = in.readInt(); + + if (in.readableBytes() < length) { + in.resetReaderIndex(); + return; + } + + byte[] compressed = new byte[length]; + in.readBytes(compressed); + + var time = System.currentTimeMillis(); + byte[] decompressed = Zstd.decompress(compressed, originalLength); +// LOGGER.info("Decompression time: {}ms. Saved {} bytes", System.currentTimeMillis() - time, originalLength - length); + + ByteBuf decompressedBuf = ctx.alloc().buffer(decompressed.length); + decompressedBuf.writeBytes(decompressed); + out.add(decompressedBuf); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java new file mode 100644 index 00000000..4d2d44f8 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java @@ -0,0 +1,25 @@ +package pl.skidam.automodpack_core.netty.handler; + +import com.github.luben.zstd.Zstd; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; + +public class ZstdEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { + byte[] input = new byte[msg.readableBytes()]; + msg.readBytes(input); + + var time = System.currentTimeMillis(); + byte[] compressed = Zstd.compress(input); +// LOGGER.info("Compression time: {}ms. Saved {} bytes", System.currentTimeMillis() - time, input.length - compressed.length); + + out.writeInt(compressed.length); + out.writeInt(input.length); + out.writeBytes(compressed); + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java b/core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java new file mode 100644 index 00000000..a4d1a5a8 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java @@ -0,0 +1,22 @@ +package pl.skidam.automodpack_core.netty.message; + +import static pl.skidam.automodpack_core.netty.NetUtils.ECHO_TYPE; + +public class EchoMessage extends ProtocolMessage { + private final int dataLength; + private final byte[] data; + + public EchoMessage(byte version, byte[] secret, byte[] data) { + super(version, ECHO_TYPE, secret); + this.dataLength = data.length; + this.data = data; + } + + public int getDataLength() { + return dataLength; + } + + public byte[] getData() { + return data; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java b/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java new file mode 100644 index 00000000..8d19ceea --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java @@ -0,0 +1,22 @@ +package pl.skidam.automodpack_core.netty.message; + +import static pl.skidam.automodpack_core.netty.NetUtils.FILE_REQUEST_TYPE; + +public class FileRequestMessage extends ProtocolMessage { + private final int fileHashLength; + private final byte[] fileHash; + + public FileRequestMessage(byte version, byte[] secret, byte[] fileHash) { + super(version, FILE_REQUEST_TYPE, secret); + this.fileHashLength = fileHash.length; + this.fileHash = fileHash; + } + + public int getFileHashLength() { + return fileHashLength; + } + + public byte[] getFileHash() { + return fileHash; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java b/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java new file mode 100644 index 00000000..c4f81445 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java @@ -0,0 +1,22 @@ +package pl.skidam.automodpack_core.netty.message; + +import static pl.skidam.automodpack_core.netty.NetUtils.FILE_RESPONSE_TYPE; + +public class FileResponseMessage extends ProtocolMessage { + private final int dataLength; + private final byte[] data; + + public FileResponseMessage(byte version, byte[] secret, byte[] data) { + super(version, FILE_RESPONSE_TYPE, secret); + this.dataLength = data.length; + this.data = data; + } + + public int getDataLength() { + return dataLength; + } + + public byte[] getData() { + return data; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java b/core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java new file mode 100644 index 00000000..8f6720e0 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java @@ -0,0 +1,28 @@ +package pl.skidam.automodpack_core.netty.message; + +public abstract class ProtocolMessage { + private final byte version; // 1 byte + private final byte type; // 1 byte + private final byte[] secret; // 32 bytes + + public ProtocolMessage(byte version, byte type, byte[] secret) { + if (secret.length != 32) { + throw new IllegalArgumentException("Secret must be 32 bytes"); + } + this.version = version; + this.type = type; + this.secret = secret; + } + + public byte getVersion() { + return version; + } + + public byte getType() { + return type; + } + + public byte[] getSecret() { + return secret; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java b/core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java new file mode 100644 index 00000000..1722da44 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java @@ -0,0 +1,28 @@ +package pl.skidam.automodpack_core.netty.message; + +import static pl.skidam.automodpack_core.netty.NetUtils.REFRESH_REQUEST_TYPE; + +public class RefreshRequestMessage extends ProtocolMessage { + private final int fileHashesCount; + private final int fileHashesLength; + private final byte[][] fileHashesList; + + public RefreshRequestMessage(byte version, byte[] secret, byte[][] fileHashesList) { + super(version, REFRESH_REQUEST_TYPE, secret); + this.fileHashesCount = fileHashesList.length; + this.fileHashesLength = fileHashesList[0].length; + this.fileHashesList = fileHashesList; + } + + public int getFileHashesCount() { + return fileHashesCount; + } + + public int getFileHashesLength() { + return fileHashesLength; + } + + public byte[][] getFileHashesList() { + return fileHashesList; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java index a3980afd..ade72a3f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java @@ -63,14 +63,7 @@ public static Path getPath(Path origin, String path) { // our implementation of Files.copy, thanks to usage of RandomAccessFile we can copy files that are in use public static void copyFile(Path source, Path destination) throws IOException { - if (!Files.exists(destination)) { - if (!Files.exists(destination.getParent())) { - Files.createDirectories(destination.getParent()); - } - // Windows? #302 -// Files.createFile(destination); - destination.toFile().createNewFile(); - } + setupFilePaths(destination); try (RandomAccessFile sourceFile = new RandomAccessFile(source.toFile(), "r"); FileOutputStream destinationFile = new FileOutputStream(destination.toFile())) { @@ -88,6 +81,17 @@ public static void copyFile(Path source, Path destination) throws IOException { } } + public static void setupFilePaths(Path file) throws IOException { + if (!Files.exists(file)) { + if (!Files.exists(file.getParent())) { + Files.createDirectories(file.getParent()); + } + // Windows? #302 +// Files.createFile(destination); + file.toFile().createNewFile(); + } + } + private static boolean compareFilesByteByByte(Path path, byte[] referenceBytes) { try { long fileSize = Files.size(path); @@ -199,6 +203,9 @@ public static void dummyIT(Path file) { public static String getHash(Path file) { try { + if (!Files.exists(file)) + return null; + MessageDigest digest = MessageDigest.getInstance("SHA-1"); try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), "r")) { byte[] buffer = new byte[8192]; @@ -210,7 +217,7 @@ public static String getHash(Path file) { byte[] hashBytes = digest.digest(); return convertBytesToHex(hashBytes); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to get hash of file: {}", file, e); } return null; } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java b/core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java index ebc25e3a..79bea785 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java @@ -75,8 +75,9 @@ public static String normalizeIp(String ip) { ip = ip.substring(1); } - if (ip.contains(":")) { // Handle port or IPv6 scope - ip = ip.split(":", 2)[0]; + if (ip.contains(":")) { + int portIndex = ip.lastIndexOf(":"); + ip = ip.substring(0, portIndex); } if (ip.startsWith("[") && ip.endsWith("]")) { diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/Url.java b/core/src/main/java/pl/skidam/automodpack_core/utils/Url.java deleted file mode 100644 index 4fc71820..00000000 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/Url.java +++ /dev/null @@ -1,19 +0,0 @@ -package pl.skidam.automodpack_core.utils; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Url { - - public static String removeHttpPrefix(String inputUrl) { - String regex = "^(https?://)"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(inputUrl); - - if (matcher.find()) { - return inputUrl.substring(matcher.end()); - } else { - return inputUrl; // No match, return the original URL - } - } -} \ No newline at end of file diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java index 4532bb5d..f0eab703 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java @@ -15,6 +15,7 @@ import pl.skidam.automodpack_loader_core.mods.ModpackLoader; import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.file.*; import java.util.HashMap; import java.util.List; @@ -60,9 +61,18 @@ private void updateAll() { return; } + InetSocketAddress selectedModpackAddress; + + try { + int portIndex = selectedModpackLink.lastIndexOf(":"); + selectedModpackAddress = new InetSocketAddress(selectedModpackLink.substring(0, portIndex), Integer.parseInt(selectedModpackLink.substring(portIndex + 1))); + } catch (Exception e) { + return; + } + Secrets.Secret secret = SecretsStore.getClientSecret(clientConfig.selectedModpack); - var optionalLatestModpackContent = ModpackUtils.requestServerModpackContent(selectedModpackLink, secret); + var optionalLatestModpackContent = ModpackUtils.requestServerModpackContent(selectedModpackAddress, secret); var latestModpackContent = ConfigTools.loadModpackContent(selectedModpackDir.resolve(hostModpackContentFile.getFileName())); // Use the latest modpack content if available @@ -79,7 +89,7 @@ private void updateAll() { } // Update modpack - new ModpackUpdater().prepareUpdate(latestModpackContent, selectedModpackLink, secret, selectedModpackDir); + new ModpackUpdater().prepareUpdate(latestModpackContent, selectedModpackAddress, secret, selectedModpackDir); } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java index 1f20b42a..be250544 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java @@ -3,6 +3,7 @@ import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.loader.LoaderManagerService; +import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.platforms.ModrinthAPI; import pl.skidam.automodpack_loader_core.screen.ScreenManager; import pl.skidam.automodpack_loader_core.utils.DownloadManager; @@ -155,7 +156,7 @@ public static void installModVersion(ModrinthAPI automodpack) { downloadManager.download( automodpackUpdateJar, automodpack.SHA1Hash(), - new DownloadManager.Urls().addUrl(new DownloadManager.Url().getUrl(automodpack.downloadUrl())), + List.of(automodpack.downloadUrl()), () -> LOGGER.info("Downloaded update for AutoModpack."), () -> LOGGER.error("Failed to download update for AutoModpack.") ); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index b8aef7c7..cc92610a 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -3,16 +3,15 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.automodpack_core.utils.CustomFileUtils; -import pl.skidam.automodpack_core.utils.FileInspection; -import pl.skidam.automodpack_core.utils.MmcPackMagic; -import pl.skidam.automodpack_core.utils.WorkaroundUtil; +import pl.skidam.automodpack_core.netty.client.DownloadClient; +import pl.skidam.automodpack_core.utils.*; import pl.skidam.automodpack_loader_core.ReLauncher; import pl.skidam.automodpack_loader_core.screen.ScreenManager; import pl.skidam.automodpack_loader_core.utils.*; import java.io.IOException; import java.net.ConnectException; +import java.net.InetSocketAddress; import java.net.SocketTimeoutException; import java.nio.file.*; import java.util.*; @@ -29,10 +28,10 @@ public class ModpackUpdater { private Jsons.ModpackContentFields serverModpackContent; private String unModifiedSMC; private WorkaroundUtil workaroundUtil; - public Map failedDownloads = new HashMap<>(); + public Map> failedDownloads = new HashMap<>(); private final Set newDownloadedFiles = new HashSet<>(); // Only files which did not exist before. Because some files may have the same name/path and be updated. - private String modpackLink; + private InetSocketAddress modpackAddress; private Secrets.Secret modpackSecret; private Path modpackDir; private Path modpackContentFile; @@ -42,14 +41,14 @@ public String getModpackName() { return serverModpackContent.modpackName; } - public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link, Secrets.Secret secret, Path modpackPath) { + public void prepareUpdate(Jsons.ModpackContentFields modpackContent, InetSocketAddress address, Secrets.Secret secret, Path modpackPath) { serverModpackContent = modpackContent; - modpackLink = link; + modpackAddress = address; modpackSecret = secret; modpackDir = modpackPath; - if (modpackLink == null || modpackLink.isEmpty() || modpackPath.toString().isEmpty()) { - throw new IllegalArgumentException("Link or modpackPath is null or empty"); + if (modpackAddress == null || modpackPath.toString().isEmpty()) { + throw new IllegalArgumentException("Address or modpackPath is null or empty"); } try { @@ -96,6 +95,8 @@ public void prepareUpdate(Jsons.ModpackContentFields modpackContent, String link } public void CheckAndLoadModpack() throws Exception { + if (!Files.exists(modpackDir)) + return; boolean requiresRestart = applyModpack(); @@ -222,6 +223,8 @@ public void startUpdate() { downloadManager = new DownloadManager(totalBytesToDownload); new ScreenManager().download(downloadManager, getModpackName()); + DownloadClient downloadClient = new DownloadClient(modpackAddress, modpackSecret, Math.min(wholeQueue, 5)); + downloadManager.attachDownloadClient(downloadClient); if (wholeQueue > 0) { var randomizedList = new LinkedList<>(serverModpackContent.list); @@ -237,13 +240,11 @@ public void startUpdate() { newDownloadedFiles.add(fileName); } - DownloadManager.Urls urls = new DownloadManager.Urls(); - - urls.addUrl(new DownloadManager.Url().getUrl(modpackLink + serverSHA1).addHeader(SECRET_REQUEST_HEADER, modpackSecret.secret())); + List urls = new ArrayList<>(); - if (fetchManager.getFetchDatas().containsKey(item.sha1)) { - urls.addAllUrls(new DownloadManager.Url().getUrls(fetchManager.getFetchDatas().get(item.sha1).fetchedData().urls())); - } +// if (fetchManager.getFetchDatas().containsKey(item.sha1)) { +// urls.addAll(fetchManager.getFetchDatas().get(item.sha1).fetchedData().urls()); +// } Runnable failureCallback = () -> { failedDownloads.put(item, urls); @@ -263,11 +264,12 @@ public void startUpdate() { } downloadManager.joinAll(); - downloadManager.cancelAllAndShutdown(); LOGGER.info("Finished downloading files in {}ms", System.currentTimeMillis() - startFetching); } + downloadManager.cancelAllAndShutdown(); + totalBytesToDownload = 0; Map hashesToRefresh = new HashMap<>(); // File name, hash @@ -281,13 +283,16 @@ public void startUpdate() { LOGGER.warn("Failed to download {} files", hashesToRefresh.size()); if (!hashesToRefresh.isEmpty()) { - // make json from the hashes list - String hashesJson = GSON.toJson(hashesToRefresh.values()); + // make byte[][] from hashesToRefresh.values() + byte[][] hashesArray = hashesToRefresh.values().stream() + .map(String::getBytes) + .toArray(byte[][]::new); + // send it to the server and get the new modpack content // TODO set client to a waiting for the server to respond screen LOGGER.warn("Trying to refresh the modpack content"); - LOGGER.info("Sending hashes to refresh: {}", hashesJson); - var refreshedContentOptional = ModpackUtils.refreshServerModpackContent(modpackLink, modpackSecret, hashesJson); + LOGGER.info("Sending hashes to refresh: {}", hashesToRefresh.values()); + var refreshedContentOptional = ModpackUtils.refreshServerModpackContent(modpackAddress, modpackSecret, hashesArray); if (refreshedContentOptional.isEmpty()) { LOGGER.error("Failed to refresh the modpack content"); } else { @@ -298,6 +303,7 @@ public void startUpdate() { downloadManager = new DownloadManager(totalBytesToDownload); new ScreenManager().download(downloadManager, getModpackName()); + downloadManager.attachDownloadClient(downloadClient); var refreshedContent = refreshedContentOptional.get(); this.unModifiedSMC = GSON.toJson(refreshedContent); @@ -314,20 +320,18 @@ public void startUpdate() { String serverSHA1 = item.sha1; Path downloadFile = CustomFileUtils.getPath(modpackDir, fileName); - DownloadManager.Urls urls = new DownloadManager.Urls(); - urls.addUrl(new DownloadManager.Url().getUrl(modpackLink + serverSHA1).addHeader(SECRET_REQUEST_HEADER, modpackSecret.secret())); - LOGGER.info("Retrying to download {} from {}", fileName, urls); + LOGGER.info("Retrying to download {} from {}", fileName, modpackAddress.getAddress().getHostName()); Runnable failureCallback = () -> { - failedDownloads.put(item, urls); + failedDownloads.put(item, List.of()); }; Runnable successCallback = () -> { changelogs.changesAddedList.put(downloadFile.getFileName().toString(), null); }; - downloadManager.download(downloadFile, serverSHA1, urls, successCallback, failureCallback); + downloadManager.download(downloadFile, serverSHA1, List.of(), successCallback, failureCallback); } downloadManager.joinAll(); @@ -337,6 +341,8 @@ public void startUpdate() { } } + downloadClient.close(); + LOGGER.info("Done, saving {}", modpackContentFile.getFileName().toString()); // Downloads completed @@ -372,7 +378,7 @@ public void startUpdate() { new ReLauncher(modpackDir, updateType, changelogs).restart(false); } } catch (SocketTimeoutException | ConnectException e) { - LOGGER.error("Modpack host of " + modpackLink + " is not responding", e); + LOGGER.error("Modpack host of " + modpackAddress + " is not responding", e); } catch (InterruptedException e) { LOGGER.info("Interrupted the download"); } catch (Exception e) { @@ -383,7 +389,7 @@ public void startUpdate() { // returns true if restart is required private boolean applyModpack() throws Exception { - ModpackUtils.selectModpack(modpackDir, modpackLink, newDownloadedFiles); + ModpackUtils.selectModpack(modpackDir, modpackAddress, newDownloadedFiles); Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(modpackContentFile); if (modpackContent == null) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index f8a2cd80..85cd848c 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -6,10 +6,10 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.netty.client.DownloadClient; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.FileInspection; import pl.skidam.automodpack_core.utils.ModpackContentTools; -import pl.skidam.automodpack_core.utils.Url; import java.io.*; import java.net.*; @@ -270,6 +270,7 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont String installedModpackName = clientConfig.selectedModpack; String installedModpackLink = clientConfig.installedModpacks.get(installedModpackName); + InetSocketAddress installedModpackAddress = new InetSocketAddress(installedModpackLink.split(":")[0], Integer.parseInt(installedModpackLink.split(":")[1])); String serverModpackName = serverModpackContent.modpackName; if (!serverModpackName.equals(installedModpackName) && !serverModpackName.isEmpty()) { @@ -287,7 +288,7 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont e.printStackTrace(); } - selectModpack(newModpackDir, installedModpackLink, Set.of()); + selectModpack(newModpackDir, installedModpackAddress, Set.of()); return newModpackDir; } @@ -296,11 +297,22 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont } // Returns true if value changed - public static boolean selectModpack(Path modpackDirToSelect, String modpackLinkToSelect, Set newDownloadedFiles) { + public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress modpackAddressToSelect, Set newDownloadedFiles) { final String modpackToSelect = modpackDirToSelect.getFileName().toString(); String selectedModpack = clientConfig.selectedModpack; String selectedModpackLink = clientConfig.installedModpacks.get(selectedModpack); + LOGGER.info("Selected modpack link: {}", selectedModpackLink); + + InetSocketAddress selectedModpackAddress = null; + try { + int portIndex = selectedModpackLink.lastIndexOf(":"); + selectedModpackAddress = new InetSocketAddress(selectedModpackLink.substring(0, portIndex), Integer.parseInt(selectedModpackLink.substring(portIndex + 1))); + } catch (Exception e) { + if (selectedModpackLink != null && !selectedModpackLink.isBlank()) { + LOGGER.error("Error while parsing selected modpack address", e); + } + } // Save current editable files Path selectedModpackDir = modpacksDir.resolve(selectedModpack); @@ -321,8 +333,12 @@ public static boolean selectModpack(Path modpackDirToSelect, String modpackLinkT clientConfig.selectedModpack = modpackToSelect; ConfigTools.save(clientConfigFile, clientConfig); - ModpackUtils.addModpackToList(modpackToSelect, modpackLinkToSelect); - return !Objects.equals(modpackToSelect, selectedModpack) || !Objects.equals(modpackLinkToSelect, selectedModpackLink); + ModpackUtils.addModpackToList(modpackToSelect, modpackAddressToSelect); + + LOGGER.warn("modpackToSelect: {}, selectedModpack: {}", modpackToSelect, modpackToSelect); + LOGGER.warn("modpackAddressToSelect: {}, selectedModpackAddress: {}", modpackAddressToSelect, selectedModpackAddress); + + return !Objects.equals(modpackToSelect, selectedModpack) || !Objects.equals(modpackAddressToSelect, selectedModpackAddress); } public static void removeModpackFromList(String modpackName) { @@ -338,32 +354,34 @@ public static void removeModpackFromList(String modpackName) { } } - public static void addModpackToList(String modpackName, String link) { - if (modpackName == null || modpackName.isEmpty() || link == null || link.isEmpty()) { + public static void addModpackToList(String modpackName, InetSocketAddress address) { + if (modpackName == null || modpackName.isEmpty() || address == null) { return; } Map modpacks = new HashMap<>(clientConfig.installedModpacks); - modpacks.put(modpackName, link); + String addressString = address.getAddress().getHostAddress() + ":" + address.getPort(); + modpacks.put(modpackName, addressString); clientConfig.installedModpacks = modpacks; ConfigTools.save(clientConfigFile, clientConfig); } // Returns modpack name formatted for path or url if server doesn't provide modpack name - public static Path getModpackPath(String url, String modpackName) { + public static Path getModpackPath(InetSocketAddress address, String modpackName) { - String nameFromUrl = Url.removeHttpPrefix(url); + String strAddress = address.getAddress().getHostAddress() + ":" + address.getPort(); + String correctedName = strAddress; - if (FileInspection.isInValidFileName(nameFromUrl)) { - nameFromUrl = FileInspection.fixFileName(nameFromUrl); + if (FileInspection.isInValidFileName(strAddress)) { + correctedName = FileInspection.fixFileName(strAddress); } - Path modpackDir = CustomFileUtils.getPath(modpacksDir, nameFromUrl); + Path modpackDir = CustomFileUtils.getPath(modpacksDir, correctedName); if (!modpackName.isEmpty()) { // Check if we don't have already installed modpack via this link - if (clientConfig.installedModpacks != null && clientConfig.installedModpacks.containsValue(nameFromUrl)) { + if (clientConfig.installedModpacks != null && clientConfig.installedModpacks.containsValue(correctedName)) { return modpackDir; } @@ -379,29 +397,58 @@ public static Path getModpackPath(String url, String modpackName) { return modpackDir; } - public static Optional requestServerModpackContent(String link, Secrets.Secret secret) { + public static Optional requestServerModpackContent(InetSocketAddress address, Secrets.Secret secret) { if (secret == null) return Optional.empty(); - if (link == null) { - throw new IllegalArgumentException("Link is null"); - } + if (address == null) + throw new IllegalArgumentException("Address is null"); - HttpURLConnection connection = null; + DownloadClient client = null; try { - connection = (HttpURLConnection) new URL(link).openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty(SECRET_REQUEST_HEADER, secret.secret()); + client = new DownloadClient(address, secret, 1); + var future = client.downloadFile(new byte[0], null); + var result = future.get(); + if (result instanceof List list) { + return parseStreamToModpack((List) list); + } - return connectionToModpack(connection); + return Optional.empty(); } catch (Exception e) { LOGGER.error("Error while getting server modpack content", e); } finally { - if (connection != null) { - connection.disconnect(); + if (client != null) + client.close(); + } + + return Optional.empty(); + } + + public static Optional refreshServerModpackContent(InetSocketAddress address, Secrets.Secret secret, byte[][] fileHashes) { + if (secret == null) + return Optional.empty(); + + if (address == null) + throw new IllegalArgumentException("Address is null"); + + + DownloadClient client = null; + try { + client = new DownloadClient(address, secret, 1); + var future = client.requestRefresh(fileHashes); + var result = future.get(); + if (result instanceof List list) { + return parseStreamToModpack((List) list); } + + return Optional.empty(); + } catch (Exception e) { + LOGGER.error("Error while getting server modpack content", e); + } finally { + if (client != null) + client.close(); } return Optional.empty(); @@ -467,6 +514,53 @@ public static Optional connectionToModpack(HttpURLCo return Optional.empty(); } + public static Optional parseStreamToModpack(List rawBytes) { + + String response = null; + + // get list of bytes[] to one byte[] object + long len = rawBytes.stream().mapToLong(b -> b.length).sum(); + byte[] bytes = new byte[(int) len]; + int pos = 0; + for (byte[] b : rawBytes) { + System.arraycopy(b, 0, bytes, pos, b.length); + pos += b.length; + } + + try (InputStreamReader isr = new InputStreamReader(new ByteArrayInputStream(bytes))) { + JsonElement element = new JsonParser().parse(isr); // Needed to parse by deprecated method because of older minecraft versions (<1.17.1) + if (element != null && !element.isJsonArray()) { + JsonObject obj = element.getAsJsonObject(); + response = obj.toString(); + } + } catch (Exception e) { + LOGGER.error("Couldn't parse modpack content", e); + } + + if (response == null) { + LOGGER.error("Couldn't parse modpack content"); + return Optional.empty(); + } + + Jsons.ModpackContentFields serverModpackContent = GSON.fromJson(response, Jsons.ModpackContentFields.class); + + if (serverModpackContent == null) { + LOGGER.error("Couldn't parse modpack content"); + return Optional.empty(); + } + + if (serverModpackContent.list.isEmpty()) { + LOGGER.error("Modpack content is empty!"); + return Optional.empty(); + } + + if (potentiallyMalicious(serverModpackContent)) { + return Optional.empty(); + } + + return Optional.of(serverModpackContent); + } + public static Optional parseStreamToModpack(InputStream stream) { String response = null; diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 9b42e605..99dda10e 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -1,11 +1,13 @@ package pl.skidam.automodpack_loader_core.utils; +import pl.skidam.automodpack_core.netty.client.DownloadClient; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; import pl.skidam.automodpack_core.utils.FileInspection; import java.io.*; import java.net.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -19,6 +21,7 @@ public class DownloadManager { private static final int MAX_DOWNLOAD_ATTEMPTS = 2; // its actually 3, but we start from 0 private static final int BUFFER_SIZE = 128 * 1024; private final ExecutorService DOWNLOAD_EXECUTOR = Executors.newFixedThreadPool(MAX_DOWNLOADS_IN_PROGRESS, new CustomThreadFactoryBuilder().setNameFormat("AutoModpackDownload-%d").build()); + private DownloadClient downloadClient = null; private final Map queuedDownloads = new ConcurrentHashMap<>(); public final Map downloadsInProgress = new ConcurrentHashMap<>(); private long bytesDownloaded = 0; @@ -33,26 +36,37 @@ public DownloadManager(long bytesToDownload) { } // TODO: make caching system which detects if the same file was downloaded before and if so copy it instead of downloading again - public void download(Path file, String sha1, Urls urls, Runnable successCallback, Runnable failureCallback) { + public void attachDownloadClient(DownloadClient downloadClient) { + this.downloadClient = downloadClient; + } + + public void download(Path file, String sha1, List urls, Runnable successCallback, Runnable failureCallback) { FileInspection.HashPathPair hashPathPair = new FileInspection.HashPathPair(sha1, file); - if (queuedDownloads.containsKey(hashPathPair)) return; + if (queuedDownloads.containsKey(hashPathPair)) return; queuedDownloads.put(hashPathPair, new QueuedDownload(file, urls, 0, successCallback, failureCallback)); addedToQueue++; downloadNext(); } - private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) { - LOGGER.info("Downloading {} - {}", queuedDownload.file.getFileName(), queuedDownload.urls.toString()); + private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws Exception { + LOGGER.info("Downloading {} - {}", queuedDownload.file.getFileName(), queuedDownload.urls); - int numberOfIndexes = queuedDownload.urls.numberOfUrls - 1; + int numberOfIndexes = queuedDownload.urls.size(); int urlIndex = Math.min(queuedDownload.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes); - - Url url = queuedDownload.urls.URLs.get(numberOfIndexes - urlIndex); - + String url = null; + if (queuedDownload.urls.size() > urlIndex) { // avoids IndexOutOfBoundsException + url = queuedDownload.urls.get(urlIndex); + } boolean interrupted = false; try { - downloadFile(url, hashPathPair, queuedDownload); + if (url != null && queuedDownload.attempts < MAX_DOWNLOAD_ATTEMPTS * numberOfIndexes) { + httpDownloadFile(url, hashPathPair, queuedDownload); + } else if (downloadClient != null) { + hostDownloadFile(hashPathPair, queuedDownload); + } else { + LOGGER.error("No download client attached, can't download file - {}", queuedDownload.file.getFileName()); + } } catch (InterruptedException e) { interrupted = true; } catch (SocketTimeoutException e) { @@ -73,7 +87,7 @@ private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownlo // Runs on success failed = false; downloaded++; - LOGGER.info("Successfully downloaded {} from {}", queuedDownload.file.getFileName(), url.url); + LOGGER.info("Successfully downloaded {} from {}", queuedDownload.file.getFileName(), url); queuedDownload.successCallback.run(); semaphore.release(); } @@ -84,7 +98,7 @@ private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownlo CustomFileUtils.forceDelete(queuedDownload.file); if (!interrupted) { - if (queuedDownload.attempts < queuedDownload.urls.numberOfUrls * MAX_DOWNLOAD_ATTEMPTS) { + if (queuedDownload.attempts < (numberOfIndexes + 1) * MAX_DOWNLOAD_ATTEMPTS) { LOGGER.warn("Download of {} failed, retrying!", queuedDownload.file.getFileName()); queuedDownload.attempts++; synchronized (queuedDownloads) { @@ -113,7 +127,13 @@ private synchronized void downloadNext() { return; } - CompletableFuture future = CompletableFuture.runAsync(() -> downloadTask(hashAndPath, queuedDownload), DOWNLOAD_EXECUTOR); + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + downloadTask(hashAndPath, queuedDownload); + } catch (Exception e) { + e.printStackTrace(); + } + }, DOWNLOAD_EXECUTOR); synchronized (downloadsInProgress) { downloadsInProgress.put(hashAndPath, new DownloadData(future, queuedDownload.file)); @@ -121,7 +141,32 @@ private synchronized void downloadNext() { } } - private void downloadFile(Url url, FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException { + private void hostDownloadFile(FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException, ExecutionException { + Path outFile = queuedDownload.file; + + if (Files.exists(outFile)) { + if (Objects.equals(hashPathPair.hash(), CustomFileUtils.getHash(outFile))) { + return; + } else { + CustomFileUtils.forceDelete(outFile); + } + } + + if (outFile.getParent() != null) { + Files.createDirectories(outFile.getParent()); + } + + if (!Files.exists(outFile)) { + // Windows? #302 + outFile.toFile().createNewFile(); +// Files.createFile(outFile); + } + + var future = downloadClient.downloadFile(hashPathPair.hash().getBytes(StandardCharsets.UTF_8), outFile); + future.join(); + } + + private void httpDownloadFile(String url, FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException { Path outFile = queuedDownload.file; @@ -143,7 +188,7 @@ private void downloadFile(Url url, FileInspection.HashPathPair hashPathPair, Que // Files.createFile(outFile); } - URLConnection connection = getUrlConnection(url); + URLConnection connection = getHttpConnection(url); try (OutputStream outputStream = new FileOutputStream(outFile.toFile()); InputStream rawInputStream = new BufferedInputStream(connection.getInputStream(), BUFFER_SIZE); @@ -164,12 +209,12 @@ private void downloadFile(Url url, FileInspection.HashPathPair hashPathPair, Que } } - private URLConnection getUrlConnection(Url url) throws IOException { - URL connectionUrl = new URL(url.url); + private URLConnection getHttpConnection(String url) throws IOException { + + LOGGER.info("Downloading from {}", url); + + URL connectionUrl = new URL(url); URLConnection connection = connectionUrl.openConnection(); - for (Map.Entry header : url.headers.entrySet()) { - connection.addRequestProperty(header.getKey(), header.getValue()); - } connection.addRequestProperty("Accept-Encoding", "gzip"); connection.addRequestProperty("User-Agent", "github/skidamek/automodpack/" + AM_VERSION); connection.setConnectTimeout(10000); @@ -237,65 +282,14 @@ public void cancelAllAndShutdown() { } } - // TODO re-write it as this consumes too much code lol - public static class Url { - private final Map headers = new HashMap<>(); - private String url; - - public Url getUrl(String url) { - this.url = url; - return this; - } - - public List getUrls(List urls) { - List urlList = new ArrayList<>(); - urls.forEach((url) -> { - urlList.add(new Url().getUrl(url)); - }); - return urlList; - } - - public Url addHeader(String headerName, String header) { - headers.put(headerName, header); - return this; - } - } - - public static class Urls { - private final List URLs = new ArrayList<>(3); - private int numberOfUrls; - - public Urls addUrl(Url url) { - URLs.add(url); - numberOfUrls = URLs.size(); - return this; - } - - public Urls addAllUrls(List urls) { - URLs.addAll(urls); - numberOfUrls = URLs.size(); - return this; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - URLs.forEach((url) -> { - sb.append(url.url).append(", "); - }); - sb.delete(sb.length() - 2, sb.length()); - String str = sb.toString(); - return "[" + str + "]"; - } - } public static class QueuedDownload { private final Path file; - private final Urls urls; + private final List urls; private int attempts; private final Runnable successCallback; private final Runnable failureCallback; - public QueuedDownload(Path file, Urls urls, int attempts, Runnable successCallback, Runnable failureCallback) { + public QueuedDownload(Path file, List urls, int attempts, Runnable successCallback, Runnable failureCallback) { this.file = file; this.urls = urls; this.attempts = attempts; diff --git a/loader/loader-fabric-core.gradle.kts b/loader/loader-fabric-core.gradle.kts index e599e9d6..8343deea 100644 --- a/loader/loader-fabric-core.gradle.kts +++ b/loader/loader-fabric-core.gradle.kts @@ -25,6 +25,9 @@ dependencies { compileOnly("com.google.code.gson:gson:2.10.1") compileOnly("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.tomlj:tomlj:1.1.1") + implementation("org.bouncycastle:bcprov-jdk18on:1.80") + implementation("org.bouncycastle:bcpkix-jdk18on:1.80") + implementation("com.github.luben:zstd-jni:1.5.7-1") compileOnly("net.fabricmc:fabric-loader:${property("loader_fabric")}") } diff --git a/loader/loader-forge.gradle.kts b/loader/loader-forge.gradle.kts index 074a7cdd..58615de2 100644 --- a/loader/loader-forge.gradle.kts +++ b/loader/loader-forge.gradle.kts @@ -37,6 +37,9 @@ dependencies { compileOnly("com.google.code.gson:gson:2.10.1") compileOnly("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.tomlj:tomlj:1.1.1") + implementation("org.bouncycastle:bcprov-jdk18on:1.80") + implementation("org.bouncycastle:bcpkix-jdk18on:1.80") + implementation("com.github.luben:zstd-jni:1.5.7-1") if (project.name.contains("neoforge")) { "neoForge"("net.neoforged:neoforge:${property("loader_neoforge")}") diff --git a/src/main/java/pl/skidam/automodpack/client/audio/CustomSoundInstance.java b/src/main/java/pl/skidam/automodpack/client/audio/CustomSoundInstance.java index 3f7189d0..6ea9ea88 100644 --- a/src/main/java/pl/skidam/automodpack/client/audio/CustomSoundInstance.java +++ b/src/main/java/pl/skidam/automodpack/client/audio/CustomSoundInstance.java @@ -14,10 +14,10 @@ public class CustomSoundInstance extends AbstractSoundInstance { public CustomSoundInstance(Supplier event) { /*? if >=1.21.2 {*/ - super(event.get().id(), SoundCategory.MASTER, Random.create()); - /*?} elif >=1.19.1 {*/ - /*super(event.get().getId(), SoundCategory.MASTER, Random.create()); - *//*?} else {*/ + /*super(event.get().id(), SoundCategory.MASTER, Random.create()); + *//*?} elif >=1.19.1 {*/ + super(event.get().getId(), SoundCategory.MASTER, Random.create()); + /*?} else {*/ /*super(event.get().getId(), SoundCategory.MASTER); *//*?}*/ this.attenuationType = AttenuationType.NONE; diff --git a/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java index 6e99ef9a..c5b69ebd 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java @@ -25,9 +25,9 @@ /*?}*/ /*? if >=1.21.2 {*/ -import net.minecraft.client.render.RenderLayer; +/*import net.minecraft.client.render.RenderLayer; import java.util.function.Function; -/*?}*/ +*//*?}*/ public class VersionedScreen extends Screen { @@ -108,11 +108,11 @@ public static ButtonWidget buttonWidget(int x, int y, int width, int height, Tex *//*?} else {*/ public static void drawTexture(Identifier textureID, VersionedMatrices matrices, int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) { /*? if >=1.21.2 {*/ - Function renderLayers = RenderLayer::getGuiTextured; + /*Function renderLayers = RenderLayer::getGuiTextured; matrices.drawTexture(renderLayers, textureID, x, y, u, v, width, height, textureWidth, textureHeight); - /*?} else {*/ - /*matrices.drawTexture(textureID, x, y, u, v, width, height, textureWidth, textureHeight); - *//*?}*/ + *//*?} else {*/ + matrices.drawTexture(textureID, x, y, u, v, width, height, textureWidth, textureHeight); + /*?}*/ } /*?}*/ } diff --git a/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntryWidget.java b/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntryWidget.java index 74f7fd29..016c3997 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntryWidget.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntryWidget.java @@ -59,10 +59,10 @@ public ListEntryWidget(Map changelogs, MinecraftClient client, i *//*?}*/ /*? if >1.21.3 {*/ - public double getScrollAmount() { + /*public double getScrollAmount() { return this.getScrollY(); } - /*?}*/ + *//*?}*/ public final ListEntry getEntryAtPos(double x, double y) { int int_5 = MathHelper.floor(y - (double) getTop()) - this.headerHeight + (int) this.getScrollAmount() - 4; @@ -79,18 +79,18 @@ public int getTop() { } /*? if <=1.21.3 {*/ - /*@Override + @Override protected void updateScrollingState(double mouseX, double mouseY, int button) { super.updateScrollingState(mouseX, mouseY, button); this.scrolling = button == 0 && mouseX >= (double) this.getScrollbarX() && mouseX < (double) (this.getScrollbarX() + 6); } - *//*?}*/ + /*?}*/ @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { /*? if <=1.21.3 {*/ - /*this.updateScrollingState(mouseX, mouseY, button); - *//*?}*/ + this.updateScrollingState(mouseX, mouseY, button); + /*?}*/ if (!this.isMouseOver(mouseX, mouseY)) { return false; } else { diff --git a/src/main/java/pl/skidam/automodpack/init/Common.java b/src/main/java/pl/skidam/automodpack/init/Common.java index 0aecee36..3de2a3d4 100644 --- a/src/main/java/pl/skidam/automodpack/init/Common.java +++ b/src/main/java/pl/skidam/automodpack/init/Common.java @@ -5,8 +5,8 @@ import pl.skidam.automodpack.loader.GameCall; import pl.skidam.automodpack.networking.ModPackets; import pl.skidam.automodpack_core.modpack.Modpack; -import pl.skidam.automodpack_core.netty.HttpServer; import pl.skidam.automodpack_core.loader.LoaderManagerService; +import pl.skidam.automodpack_core.netty.NettyServer; import java.util.HashMap; import java.util.Map; @@ -42,7 +42,7 @@ public static void serverInit() { public static void init() { GAME_CALL = new GameCall(); - httpServer = new HttpServer(); + hostServer = new NettyServer(); modpack = new Modpack(); } @@ -51,7 +51,7 @@ public static void afterSetupServer() { return; } - httpServer.start(); + hostServer.start(); } public static void beforeShutdownServer() { @@ -59,7 +59,7 @@ public static void beforeShutdownServer() { return; } - httpServer.stop(); + hostServer.stop(); modpack.shutdownExecutor(); } diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/MusicTrackerMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/MusicTrackerMixin.java index 835fec00..38f39ac6 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/MusicTrackerMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/MusicTrackerMixin.java @@ -2,10 +2,10 @@ package pl.skidam.automodpack.mixin.core; /*? if >1.21.3 {*/ -import net.minecraft.client.sound.MusicInstance; -/*?} else {*/ -/*import net.minecraft.sound.MusicSound; -*//*?}*/ +/*import net.minecraft.client.sound.MusicInstance; +*//*?} else {*/ +import net.minecraft.sound.MusicSound; +/*?}*/ import net.minecraft.client.sound.MusicTracker; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -22,10 +22,10 @@ public class MusicTrackerMixin { cancellable = true ) /*? if >1.21.3 {*/ - private void play(MusicInstance music, CallbackInfo ci) { - /*?} else {*/ - /*private void play(MusicSound type, CallbackInfo ci) { - *//*?}*/ + /*private void play(MusicInstance music, CallbackInfo ci) { + *//*?} else {*/ + private void play(MusicSound type, CallbackInfo ci) { + /*?}*/ if (AudioManager.isMusicPlaying()) { ci.cancel(); } diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java index ac62afd7..88b21ac5 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java @@ -5,9 +5,11 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import pl.skidam.automodpack_core.netty.InterConnector; +import pl.skidam.automodpack_core.GlobalVariables; +import pl.skidam.automodpack_core.netty.handler.ProtocolServerHandler; import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; +import static pl.skidam.automodpack_core.GlobalVariables.hostServer; @Mixin(targets = "net/minecraft/server/ServerNetworkIo$1", priority = 2137) public abstract class ServerNetworkIoMixin { @@ -17,6 +19,18 @@ public abstract class ServerNetworkIoMixin { at = @At("TAIL") ) private void injectAutoModpackHost(Channel channel, CallbackInfo ci) { - channel.pipeline().addFirst(MOD_ID, new InterConnector()); + if (!GlobalVariables.serverConfig.hostModpackOnMinecraftPort) { + return; + } + + if (!GlobalVariables.serverConfig.modpackHost) { + return; + } + + if (!hostServer.shouldHost()) { + return; + } + + channel.pipeline().addFirst(MOD_ID, new ProtocolServerHandler(GlobalVariables.hostServer.getSslCtx())); } } diff --git a/src/main/java/pl/skidam/automodpack/modpack/Commands.java b/src/main/java/pl/skidam/automodpack/modpack/Commands.java index 9705c9ee..40ad1e35 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/Commands.java +++ b/src/main/java/pl/skidam/automodpack/modpack/Commands.java @@ -66,10 +66,10 @@ private static int reload(CommandContext context) { private static int startModpackHost(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { - if (!httpServer.shouldRunInternally()) { + if (!hostServer.shouldRunInternally()) { send(context, "Starting modpack hosting...", Formatting.YELLOW, true); - httpServer.start(); - if (httpServer.shouldRunInternally()) { + hostServer.start(); + if (hostServer.shouldRunInternally()) { send(context, "Modpack hosting started!", Formatting.GREEN, true); } else { send(context, "Couldn't start server!", Formatting.RED, true); @@ -84,9 +84,9 @@ private static int startModpackHost(CommandContext context) private static int stopModpackHost(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { - if (httpServer.shouldRunInternally()) { + if (hostServer.shouldRunInternally()) { send(context, "Stopping modpack hosting...", Formatting.RED, true); - if (httpServer.stop()) { + if (hostServer.stop()) { send(context, "Modpack hosting stopped!", Formatting.RED, true); } else { send(context, "Couldn't stop server!", Formatting.RED, true); @@ -102,17 +102,17 @@ private static int stopModpackHost(CommandContext context) private static int restartModpackHost(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { send(context, "Restarting modpack hosting...", Formatting.YELLOW, true); - boolean needStop = httpServer.shouldRunInternally(); + boolean needStop = hostServer.shouldRunInternally(); boolean stopped = false; if (needStop) { - stopped = httpServer.stop(); + stopped = hostServer.stop(); } if (needStop && !stopped) { send(context, "Couldn't restart server!", Formatting.RED, true); } else { - httpServer.start(); - if (httpServer.shouldRunInternally()) { + hostServer.start(); + if (hostServer.shouldRunInternally()) { send(context, "Modpack hosting restarted!", Formatting.GREEN, true); } else { send(context, "Couldn't restart server!", Formatting.RED, true); @@ -125,8 +125,8 @@ private static int restartModpackHost(CommandContext contex private static int modpackHostAbout(CommandContext context) { - Formatting statusColor = httpServer.shouldRunInternally() ? Formatting.GREEN : Formatting.RED; - String status = httpServer.shouldRunInternally() ? "running" : "not running"; + Formatting statusColor = hostServer.shouldRunInternally() ? Formatting.GREEN : Formatting.RED; + String status = hostServer.shouldRunInternally() ? "running" : "not running"; send(context, "Modpack hosting status", Formatting.GREEN, status, statusColor, false); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java b/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java index 43ed33a5..aa2775d8 100644 --- a/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/content/DataPacket.java @@ -4,13 +4,15 @@ import pl.skidam.automodpack_core.auth.Secrets; public class DataPacket { - public String link; + public String address; + public Integer port; public String modpackName; public Secrets.Secret secret; public boolean modRequired; - public DataPacket(String link, String modpackName, Secrets.Secret secret, boolean modRequired) { - this.link = link; + public DataPacket(String address, Integer port, String modpackName, Secrets.Secret secret, boolean modRequired) { + this.address = address; + this.port = port; this.modpackName = modpackName; this.secret = secret; this.modRequired = modRequired; diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index 42cd6d6b..b82d5fa2 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -14,8 +14,6 @@ import pl.skidam.automodpack_loader_core.client.ModpackUtils; import pl.skidam.automodpack_loader_core.utils.UpdateType; -import java.net.Inet6Address; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.file.Path; import java.util.Set; @@ -28,7 +26,8 @@ public static CompletableFuture receive(MinecraftClient minecraft String serverResponse = buf.readString(Short.MAX_VALUE); DataPacket dataPacket = DataPacket.fromJson(serverResponse); - String link = dataPacket.link; + String packetAddress = dataPacket.address; + Integer packetPort = dataPacket.port; boolean modRequired = dataPacket.modRequired; if (modRequired) { @@ -37,27 +36,23 @@ public static CompletableFuture receive(MinecraftClient minecraft // 2. Dont disconnect and join server } - if (link.isBlank()) { - InetSocketAddress socketAddress = (InetSocketAddress) ((ClientLoginNetworkHandlerAccessor) handler).getConnection().getAddress(); - InetAddress inetAddress = socketAddress.getAddress(); - String ipAddress = inetAddress.getHostAddress(); - int port = socketAddress.getPort(); + InetSocketAddress address = (InetSocketAddress) ((ClientLoginNetworkHandlerAccessor) handler).getConnection().getAddress(); - if (inetAddress instanceof Inet6Address) { - ipAddress = "[" + ipAddress + "]"; - } - - link = "http://" + ipAddress + ":" + port; - LOGGER.info("Http url from connected server: {}", link); + if (packetAddress.isBlank()) { + LOGGER.info("Address from connected server: {}:{}", address.getAddress().getHostName(), address.getPort()); + } else if (packetPort != null) { + address = new InetSocketAddress(packetAddress, packetPort); + LOGGER.info("Received address packet from server! {}:{}", packetAddress, packetPort); } else { - LOGGER.info("Received link packet from server! {}", link); + var portIndex = packetAddress.lastIndexOf(':'); + var port = portIndex == -1 ? 0 : Integer.parseInt(packetAddress.substring(portIndex + 1)); + var addressString = portIndex == -1 ? packetAddress : packetAddress.substring(0, portIndex); + address = new InetSocketAddress(addressString, port); + LOGGER.info("Received address packet from server! {} Attached port: {}", addressString, port); } - // TODO: dont require/hardcode this - link = link + "/automodpack/"; - - Path modpackDir = ModpackUtils.getModpackPath(link, dataPacket.modpackName); - boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, link, Set.of()); + Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); + boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); // save secret Secrets.Secret secret = dataPacket.secret; @@ -65,14 +60,14 @@ public static CompletableFuture receive(MinecraftClient minecraft Boolean needsDisconnecting = null; - var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(link, secret); + var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(address, secret); if (optionalServerModpackContent.isPresent()) { boolean update = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); if (update) { disconnectImmediately(handler); - new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), link, secret, modpackDir); + new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), address, secret, modpackDir); needsDisconnecting = true; } else if (selectedModpackChanged) { disconnectImmediately(handler); diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index ef82c07f..34b32fd4 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -34,6 +34,10 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han LOGGER.error("Player {} doesn't have UUID: {}", playerName, profile.getId()); } + if (!connection.isEncrypted()) { + LOGGER.warn("Connection is not encrypted for player: {}", playerName); + } + if (server.getPlayerManager().checkCanJoin(connection.getAddress(), profile) != null) { return; // ignore it } @@ -87,7 +91,7 @@ public static void handleHandshake(ClientConnection connection, GameProfile prof return; } - if (!httpServer.shouldRunInternally()) { + if (!hostServer.shouldRunInternally()) { return; } @@ -99,16 +103,13 @@ public static void handleHandshake(ClientConnection connection, GameProfile prof } String playerIp = connection.getAddress().toString(); + String addressToSend; - String linkToSend; - - // If the player is connecting locally or their IP matches a specified IP, use the local host IP and port - String formattedPlayerIp = Ip.normalizeIp(playerIp); - - if (Ip.isLocal(formattedPlayerIp)) { // local - linkToSend = serverConfig.hostLocalIp; - } else { // Otherwise, use the public host IP and port - linkToSend = serverConfig.hostIp; + // If the player is connecting locally, use the local host IP + if (Ip.isLocal(playerIp)) { + addressToSend = serverConfig.hostLocalIp; + } else { + addressToSend = serverConfig.hostIp; } // now we know player is authenticated, packets are encrypted and player is whitelisted @@ -117,33 +118,30 @@ public static void handleHandshake(ClientConnection connection, GameProfile prof SecretsStore.saveHostSecret(profile.getId().toString(), secret); // We send empty string if hostIp/hostLocalIp is not specified in server config. Client will use ip by which it connected to the server in first place. - DataPacket dataPacket = new DataPacket("", serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); + DataPacket dataPacket = new DataPacket(addressToSend, null, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); - if (linkToSend != null && !linkToSend.isBlank()) { - if (!linkToSend.startsWith("http://") && !linkToSend.startsWith("https://")) { - linkToSend = "http://" + linkToSend; + if (serverConfig.reverseProxy) { + // With reverse proxy we dont append port to the link, it should be already included in the link + // But we need to check if the port is set in the config, since that's where modpack is actually hosted + if (serverConfig.hostPort == -1 && !serverConfig.hostModpackOnMinecraftPort) { + LOGGER.error("Reverse proxy is enabled but host port is not set in config! Please set it manually."); } - if (serverConfig.reverseProxy) { - // With reverse proxy we dont append port to the link, it should be already included in the link - // But we need to check if the port is set in the config, since that's where modpack is actually hosted + LOGGER.info("Sending {} modpack url: {}", profile.getName(), addressToSend); + } else { // Append server port + int portToSend; + if (serverConfig.hostModpackOnMinecraftPort) { + portToSend = minecraftServerPort; + } else { + portToSend = serverConfig.hostPort; + if (serverConfig.hostPort == -1) { - LOGGER.error("Reverse proxy is enabled but host port is not set in config! Please set it manually."); - } - } else { // Append server port - if (serverConfig.hostModpackOnMinecraftPort) { - linkToSend += ":" + minecraftServerPort; - } else { - linkToSend += ":" + serverConfig.hostPort; - - if (serverConfig.hostPort == -1) { - LOGGER.error("Host port is not set in config! Please set it manually."); - } + LOGGER.error("Host port is not set in config! Please set it manually."); } } - LOGGER.info("Sending {} modpack link: {}", profile.getName(), linkToSend); - dataPacket = new DataPacket(linkToSend, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); + LOGGER.info("Sending {} modpack url: {}:{}", profile.getName(), addressToSend, portToSend); + dataPacket = new DataPacket(addressToSend, portToSend, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); } String packetContentJson = dataPacket.toJson(); diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index e96a29ea..9dcc74c7 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -9,7 +9,7 @@ plugins { id("dev.kikugie.stonecutter") } -stonecutter active "1.21.4-fabric" /* [SC] DO NOT EDIT */ +stonecutter active "1.21.1-fabric" /* [SC] DO NOT EDIT */ stonecutter registerChiseled tasks.register("chiseledBuild", stonecutter.chiseled) { group = "project" From 9d25a000e4dd3cabe95130c18dcb288eb6262120 Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 23 Feb 2025 15:54:57 +0100 Subject: [PATCH 08/50] Add modrinth/cf urls back whoops --- .../automodpack_loader_core/client/ModpackUpdater.java | 7 +++---- .../automodpack_loader_core/client/ModpackUtils.java | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index cc92610a..9edf40f8 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -241,10 +241,9 @@ public void startUpdate() { } List urls = new ArrayList<>(); - -// if (fetchManager.getFetchDatas().containsKey(item.sha1)) { -// urls.addAll(fetchManager.getFetchDatas().get(item.sha1).fetchedData().urls()); -// } + if (fetchManager.getFetchDatas().containsKey(item.sha1)) { + urls.addAll(fetchManager.getFetchDatas().get(item.sha1).fetchedData().urls()); + } Runnable failureCallback = () -> { failedDownloads.put(item, urls); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 85cd848c..02e51d9d 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -302,7 +302,7 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m String selectedModpack = clientConfig.selectedModpack; String selectedModpackLink = clientConfig.installedModpacks.get(selectedModpack); - LOGGER.info("Selected modpack link: {}", selectedModpackLink); +// LOGGER.info("Selected modpack link: {}", selectedModpackLink); InetSocketAddress selectedModpackAddress = null; try { @@ -335,8 +335,8 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m ConfigTools.save(clientConfigFile, clientConfig); ModpackUtils.addModpackToList(modpackToSelect, modpackAddressToSelect); - LOGGER.warn("modpackToSelect: {}, selectedModpack: {}", modpackToSelect, modpackToSelect); - LOGGER.warn("modpackAddressToSelect: {}, selectedModpackAddress: {}", modpackAddressToSelect, selectedModpackAddress); +// LOGGER.warn("modpackToSelect: {}, selectedModpack: {}", modpackToSelect, modpackToSelect); +// LOGGER.warn("modpackAddressToSelect: {}, selectedModpackAddress: {}", modpackAddressToSelect, selectedModpackAddress); return !Objects.equals(modpackToSelect, selectedModpack) || !Objects.equals(modpackAddressToSelect, selectedModpackAddress); } From f3ebc28ef40990a02f7a0ba9d0c4e9b69ea552f2 Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 23 Feb 2025 23:51:39 +0100 Subject: [PATCH 09/50] Call `checkCanJoin` on server thread ref: #319 --- .../skidam/automodpack/loader/GameCall.java | 7 +++++-- .../networking/packet/HandshakeS2CPacket.java | 20 +++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/loader/GameCall.java b/src/main/java/pl/skidam/automodpack/loader/GameCall.java index d88382e0..0157132c 100644 --- a/src/main/java/pl/skidam/automodpack/loader/GameCall.java +++ b/src/main/java/pl/skidam/automodpack/loader/GameCall.java @@ -7,6 +7,7 @@ import java.net.SocketAddress; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -28,8 +29,10 @@ public boolean canPlayerJoin(SocketAddress address, String id) { return true; } - boolean valid = Common.server.getPlayerManager().checkCanJoin(address, profile) == null; + AtomicBoolean canJoin = new AtomicBoolean(false); + GameProfile finalProfile = profile; + Common.server.submitAndJoin(() -> canJoin.set(Common.server.getPlayerManager().checkCanJoin(address, finalProfile) == null)); - return valid; + return canJoin.get(); } } diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index 34b32fd4..b5b167fc 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -19,6 +19,8 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.utils.Ip; +import java.util.concurrent.atomic.AtomicBoolean; + import static pl.skidam.automodpack.networking.ModPackets.DATA; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -38,20 +40,12 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han LOGGER.warn("Connection is not encrypted for player: {}", playerName); } - if (server.getPlayerManager().checkCanJoin(connection.getAddress(), profile) != null) { - return; // ignore it - } + AtomicBoolean canJoin = new AtomicBoolean(false); + server.submitAndJoin(() -> canJoin.set(server.getPlayerManager().checkCanJoin(connection.getAddress(), profile) == null)); -// TODO: send this packet only if player can join (isnt banned, is whitelisted, etc.) -// at the moment it's not possible because of -// 'Cannot invoke "java.util.UUID.toString()" because the return value of "com.mojang.authlib.GameProfile.getId()" is null' -// -// SocketAddress playerIp = connection.getAddress(); -// -// if (server.getPlayerManager().checkCanJoin(playerIp, profile) != null) { -// LOGGER.info("Not providing modpack for {}", playerName); -// return; -// } + if (!canJoin.get()) { + return; + } if (!understood) { Common.players.put(playerName, false); From 9e9c124c89e7a3da2f38d46b509674583b856ade Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 24 Feb 2025 10:46:34 +0100 Subject: [PATCH 10/50] Save busy states of channels and make sure to use only free ones --- .../netty/client/DownloadClient.java | 63 +++++++++++-------- .../netty/client/EchoClient.java | 21 +++---- .../netty/client/NettyClient.java | 1 + .../netty/handler/ProtocolClientHandler.java | 6 ++ 4 files changed, 52 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java index ff629aab..c748df71 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java @@ -18,24 +18,17 @@ import javax.net.ssl.SSLException; import java.net.InetSocketAddress; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicBoolean; import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMMC; public class DownloadClient extends NettyClient { - private final List channels = new ArrayList<>(); - private final AtomicInteger roundRobinIndex = new AtomicInteger(0); + private final Map channels = new HashMap<>(); // channel, isBusy private final EventLoopGroup group; - private final Bootstrap bootstrap; - private final int poolSize; - private final InetSocketAddress remoteAddress; private final SslContext sslCtx; private final Secrets.Secret secret; private final DownloadClient downloadClient; @@ -43,9 +36,7 @@ public class DownloadClient extends NettyClient { public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, int poolSize) throws InterruptedException, SSLException { this.downloadClient = this; - this.remoteAddress = remoteAddress; this.secret = secret; - this.poolSize = poolSize; // Yes, we use the insecure because server uses self-signed cert and we have different way to verify the authenticity // Via secret and fingerprint, so the encryption strength should be the same, correct me if I'm wrong, thanks @@ -60,7 +51,7 @@ public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, in .build(); group = new NioEventLoopGroup(); - bootstrap = new Bootstrap(); + Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.SO_KEEPALIVE, true) @@ -92,7 +83,12 @@ public void secureInit(ChannelHandlerContext ctx) { @Override public void addChannel(Channel channel) { - channels.add(channel); + channels.put(channel, new AtomicBoolean(false)); + } + + @Override + public void removeChannel(Channel channel) { + channels.remove(channel); } @Override @@ -110,14 +106,19 @@ public Secrets.Secret getSecret() { * Returns a CompletableFuture that completes when the download finishes. */ public CompletableFuture downloadFile(byte[] fileHash, Path destination) { + // Select first not busy channel + Channel channel = channels.entrySet().stream() + .filter(entry -> !entry.getValue().get()) + .findFirst() + .map(Map.Entry::getKey) + .orElseThrow(() -> new IllegalStateException("No available channels")); - // Select a channel via round-robin. - int index = roundRobinIndex.getAndIncrement(); - Channel channel = channels.get(index % channels.size()); + // Mark channel as busy + channels.get(channel).set(true); // Add a new FileDownloadHandler to process this download. FileDownloadHandler downloadHandler = new FileDownloadHandler(destination); - channel.pipeline().addLast("downloadHandler-" + index, downloadHandler); + channel.pipeline().addLast("download-handler", downloadHandler); byte[] bsecret = Base64.getUrlDecoder().decode(secret.secret()); @@ -126,7 +127,10 @@ public CompletableFuture downloadFile(byte[] fileHash, Path destination) channel.writeAndFlush(request); // Return the future that will complete when the download finishes. - return downloadHandler.getDownloadFuture(); + return downloadHandler.getDownloadFuture().whenComplete((result, throwable) -> { + // Mark channel as not busy + channels.get(channel).set(false); + }); } /** @@ -134,13 +138,19 @@ public CompletableFuture downloadFile(byte[] fileHash, Path destination) * Returns a CompletableFuture that completes when the download finishes. */ public CompletableFuture requestRefresh(byte[][] fileHashes) { - // Select a channel via round-robin. - int index = roundRobinIndex.getAndIncrement(); - Channel channel = channels.get(index % channels.size()); + // Select first not busy channel + Channel channel = channels.entrySet().stream() + .filter(entry -> !entry.getValue().get()) + .findFirst() + .map(Map.Entry::getKey) + .orElseThrow(() -> new IllegalStateException("No available channels")); + + // Mark channel as busy + channels.get(channel).set(true); // Add a new FileDownloadHandler to process this download. FileDownloadHandler downloadHandler = new FileDownloadHandler(null); - channel.pipeline().addLast("downloadHandler-" + index, downloadHandler); + channel.pipeline().addLast("download-handler", downloadHandler); byte[] bsecret = Base64.getUrlDecoder().decode(secret.secret()); @@ -149,14 +159,17 @@ public CompletableFuture requestRefresh(byte[][] fileHashes) { channel.writeAndFlush(request); // Return the future that will complete when the download finishes. - return downloadHandler.getDownloadFuture(); + return downloadHandler.getDownloadFuture().whenComplete((result, throwable) -> { + // Mark channel as not busy + channels.get(channel).set(false); + }); } /** * Closes all channels in the pool and shuts down the event loop. */ public void close() { - for (Channel channel : channels) { + for (Channel channel : channels.keySet()) { if (channel.isOpen()) { channel.close(); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java b/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java index b03d78a3..83823aa3 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java @@ -2,7 +2,6 @@ import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; @@ -22,23 +21,18 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMMC; public class EchoClient extends NettyClient { private final List channels = new ArrayList<>(); - private final AtomicInteger roundRobinIndex = new AtomicInteger(0); private final EventLoopGroup group; - private final Bootstrap bootstrap; - private final InetSocketAddress remoteAddress; private final SslContext sslCtx; private final EchoClient echoClient; private final Semaphore channelLock = new Semaphore(0); public EchoClient(InetSocketAddress remoteAddress) throws InterruptedException, SSLException { this.echoClient = this; - this.remoteAddress = remoteAddress; // Yes, we use the insecure because server uses self-signed cert and we have different way to verify the authenticity // Via secret and fingerprint, so the encryption strength should be the same, correct me if I'm wrong, thanks @@ -52,12 +46,8 @@ public EchoClient(InetSocketAddress remoteAddress) throws InterruptedException, "TLS_CHACHA20_POLY1305_SHA256")) .build(); - String[] enabledProtocols = sslCtx.newEngine(ByteBufAllocator.DEFAULT).getEnabledProtocols(); - System.out.println("Enabled protocols: " + String.join(", ", enabledProtocols)); - System.out.println("Secure SslContext created using cipher suites: " + String.join(", ", sslCtx.cipherSuites())); - group = new NioEventLoopGroup(); - bootstrap = new Bootstrap(); + Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.SO_KEEPALIVE, true) @@ -87,6 +77,11 @@ public void addChannel(Channel channel) { channels.add(channel); } + @Override + public void removeChannel(Channel channel) { + channels.remove(channel); + } + @Override public void releaseChannel() { channelLock.release(); @@ -102,9 +97,7 @@ public Secrets.Secret getSecret() { * Returns a CompletableFuture that completes when the download finishes. */ public CompletableFuture sendEcho(byte[] secret, byte[] data) { - // Select a channel via round-robin. - int index = roundRobinIndex.getAndIncrement(); - Channel channel = channels.get(index % channels.size()); + Channel channel = channels.get(0); // Build and send the file request (which carries the secret and file hash). EchoMessage request = new EchoMessage((byte) 1, secret, data); diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java b/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java index f72a8c91..bca2f2ff 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java @@ -9,6 +9,7 @@ public abstract class NettyClient { public abstract SslContext getSslCtx(); public abstract void secureInit(ChannelHandlerContext ctx); public abstract void addChannel(Channel channel); + public abstract void removeChannel(Channel channel); public abstract void releaseChannel(); public abstract Secrets.Secret getSecret(); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java index 7f6c45e4..a0dbc34f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java @@ -82,6 +82,12 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t } } + @Override + public void channelInactive(ChannelHandlerContext ctx) { + client.removeChannel(ctx.channel()); + client.releaseChannel(); + } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); From 0cf2be21dbc99a7cd9bb60638459ad7b2561fbc3 Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 24 Feb 2025 11:05:10 +0100 Subject: [PATCH 11/50] Change authorization method ref: #319 --- .../skidam/automodpack/loader/GameCall.java | 8 ++----- .../automodpack/modpack/GameHelpers.java | 24 +++++++++++++++++++ .../networking/packet/HandshakeS2CPacket.java | 8 ++----- 3 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java diff --git a/src/main/java/pl/skidam/automodpack/loader/GameCall.java b/src/main/java/pl/skidam/automodpack/loader/GameCall.java index 0157132c..f67047f4 100644 --- a/src/main/java/pl/skidam/automodpack/loader/GameCall.java +++ b/src/main/java/pl/skidam/automodpack/loader/GameCall.java @@ -3,11 +3,11 @@ import com.mojang.authlib.GameProfile; import net.minecraft.util.UserCache; import pl.skidam.automodpack.init.Common; +import pl.skidam.automodpack.modpack.GameHelpers; import pl.skidam.automodpack_core.loader.GameCallService; import java.net.SocketAddress; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -29,10 +29,6 @@ public boolean canPlayerJoin(SocketAddress address, String id) { return true; } - AtomicBoolean canJoin = new AtomicBoolean(false); - GameProfile finalProfile = profile; - Common.server.submitAndJoin(() -> canJoin.set(Common.server.getPlayerManager().checkCanJoin(address, finalProfile) == null)); - - return canJoin.get(); + return GameHelpers.isPlayerAuthorized(address, profile); } } diff --git a/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java b/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java new file mode 100644 index 00000000..52cad93f --- /dev/null +++ b/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java @@ -0,0 +1,24 @@ +package pl.skidam.automodpack.modpack; + +import com.mojang.authlib.GameProfile; + +import java.net.SocketAddress; + +import static pl.skidam.automodpack.init.Common.server; + +public class GameHelpers { + + // Simpler version of `PlayerManager.checkCanJoin` + public static boolean isPlayerAuthorized(SocketAddress address, GameProfile profile) { + var playerManager = server.getPlayerManager(); + if (playerManager.getUserBanList().contains(profile)) { + return false; + } else if (!playerManager.isWhitelisted(profile)) { + return false; + } else if (playerManager.getIpBanList().isBanned(address)) { + return false; + } + + return true; + } +} diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index b5b167fc..de3055d4 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -11,6 +11,7 @@ import pl.skidam.automodpack.client.ui.versioned.VersionedText; import pl.skidam.automodpack.init.Common; import pl.skidam.automodpack.mixin.core.ServerLoginNetworkHandlerAccessor; +import pl.skidam.automodpack.modpack.GameHelpers; import pl.skidam.automodpack.networking.content.DataPacket; import pl.skidam.automodpack.networking.content.HandshakePacket; import pl.skidam.automodpack.networking.PacketSender; @@ -19,8 +20,6 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.utils.Ip; -import java.util.concurrent.atomic.AtomicBoolean; - import static pl.skidam.automodpack.networking.ModPackets.DATA; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -40,10 +39,7 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han LOGGER.warn("Connection is not encrypted for player: {}", playerName); } - AtomicBoolean canJoin = new AtomicBoolean(false); - server.submitAndJoin(() -> canJoin.set(server.getPlayerManager().checkCanJoin(connection.getAddress(), profile) == null)); - - if (!canJoin.get()) { + if (!GameHelpers.isPlayerAuthorized(connection.getAddress(), profile)) { return; } From e148e3a0049064af1852157c6d086270ac9596e4 Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 24 Feb 2025 13:46:02 +0100 Subject: [PATCH 12/50] Write download client without use of netty ref: #324 send bigger chunks of data for optimal compression ratio --- .../automodpack_core/GlobalVariables.java | 2 +- .../skidam/automodpack_core/auth/Secrets.java | 2 +- .../utils/CustomFileUtils.java | 4 +- .../pl/skidam/protocol/DownloadClient.java | 338 ++++++++++++++++++ .../netty => protocol}/NetUtils.java | 2 +- .../netty/NettyServer.java | 5 +- .../netty/client/DownloadClient.java | 10 +- .../netty/client/EchoClient.java | 10 +- .../netty/client/NettyClient.java | 2 +- .../netty/handler/FileDownloadHandler.java | 4 +- .../netty/handler/ProtocolClientHandler.java | 8 +- .../netty/handler/ProtocolMessageDecoder.java | 14 +- .../netty/handler/ProtocolMessageEncoder.java | 6 +- .../netty/handler/ProtocolServerHandler.java | 4 +- .../netty/handler/ServerMessageHandler.java | 16 +- .../netty/handler/ZstdDecoder.java | 6 +- .../netty/handler/ZstdEncoder.java | 6 +- .../netty/message/EchoMessage.java | 4 +- .../netty/message/FileRequestMessage.java | 4 +- .../netty/message/FileResponseMessage.java | 4 +- .../netty/message/ProtocolMessage.java | 2 +- .../netty/message/RefreshRequestMessage.java | 4 +- .../client/ModpackUpdater.java | 2 +- .../client/ModpackUtils.java | 8 +- .../utils/DownloadManager.java | 6 +- .../pl/skidam/automodpack/init/Common.java | 2 +- .../skidam/automodpack/loader/GameCall.java | 6 +- .../mixin/core/ServerNetworkIoMixin.java | 2 +- 28 files changed, 409 insertions(+), 74 deletions(-) create mode 100644 core/src/main/java/pl/skidam/protocol/DownloadClient.java rename core/src/main/java/pl/skidam/{automodpack_core/netty => protocol}/NetUtils.java (99%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/NettyServer.java (98%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/client/DownloadClient.java (95%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/client/EchoClient.java (92%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/client/NettyClient.java (91%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/FileDownloadHandler.java (98%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ProtocolClientHandler.java (93%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ProtocolMessageDecoder.java (83%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ProtocolMessageEncoder.java (92%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ProtocolServerHandler.java (95%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ServerMessageHandler.java (94%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ZstdDecoder.java (86%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/handler/ZstdEncoder.java (81%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/message/EchoMessage.java (78%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/message/FileRequestMessage.java (79%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/message/FileResponseMessage.java (78%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/message/ProtocolMessage.java (92%) rename core/src/main/java/pl/skidam/{automodpack_core => protocol}/netty/message/RefreshRequestMessage.java (85%) diff --git a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java index cc374ebc..904df9ed 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java +++ b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java @@ -5,7 +5,7 @@ import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.loader.*; import pl.skidam.automodpack_core.modpack.Modpack; -import pl.skidam.automodpack_core.netty.NettyServer; +import pl.skidam.protocol.netty.NettyServer; import java.nio.file.Path; diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java index 9326f175..2ab68dfd 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java @@ -1,6 +1,6 @@ package pl.skidam.automodpack_core.auth; -import pl.skidam.automodpack_core.netty.NetUtils; +import pl.skidam.protocol.NetUtils; import java.net.SocketAddress; import java.security.SecureRandom; diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java index ade72a3f..9830a726 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java @@ -39,7 +39,7 @@ public static void forceDelete(Path file) { } catch (IOException ignored) { } - if (Files.exists(file)) { + if (Files.isRegularFile(file)) { dummyIT(file); } } @@ -203,7 +203,7 @@ public static void dummyIT(Path file) { public static String getHash(Path file) { try { - if (!Files.exists(file)) + if (!Files.isRegularFile(file)) return null; MessageDigest digest = MessageDigest.getInstance("SHA-1"); diff --git a/core/src/main/java/pl/skidam/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/protocol/DownloadClient.java new file mode 100644 index 00000000..93c74618 --- /dev/null +++ b/core/src/main/java/pl/skidam/protocol/DownloadClient.java @@ -0,0 +1,338 @@ +package pl.skidam.protocol; + +import pl.skidam.automodpack_core.auth.Secrets; +import com.github.luben.zstd.Zstd; + +import javax.net.ssl.*; +import java.io.*; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import static pl.skidam.protocol.NetUtils.*; + +/** + * A DownloadClient that creates a pool of connections. + * Each connection performs an initial plain-text handshake by sending the AMMC magic, + * waiting for the AMOK reply, and then upgrading the same socket to TLSv1.3. + * Subsequent protocol messages are framed and compressed (using Zstd) to match your full protocol. + */ +public class DownloadClient { + private final List connections = new ArrayList<>(); + + public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, int poolSize) throws Exception { + for (int i = 0; i < poolSize; i++) { + connections.add(new Connection(remoteAddress, secret)); + } + } + + private synchronized Connection getFreeConnection() { + for (Connection conn : connections) { + if (!conn.isBusy()) { + conn.setBusy(true); + return conn; + } + } + throw new IllegalStateException("No available connections"); + } + + /** + * Downloads a file identified by its SHA-1 hash to the given destination. + * Returns a CompletableFuture that completes when the download finishes. + */ + public CompletableFuture downloadFile(byte[] fileHash, Path destination) { + Connection conn = getFreeConnection(); + return conn.sendDownloadFile(fileHash, destination); + } + + /** + * Sends a refresh request with the given file hashes. + */ + public CompletableFuture requestRefresh(byte[][] fileHashes) { + Connection conn = getFreeConnection(); + return conn.sendRefreshRequest(fileHashes); + } + + /** + * Closes all connections. + */ + public void close() { + for (Connection conn : connections) { + conn.close(); + } + } +} + +/** + * A helper class representing a single connection. + * It first performs a plain-text handshake then upgrades the same socket to TLS. + * Outbound messages are compressed with Zstd and framed; inbound frames are decompressed and processed. + */ +class Connection { + private static final byte PROTOCOL_VERSION = 1; + + private final byte[] secretBytes; + private final SSLSocket socket; + private final DataInputStream in; + private final DataOutputStream out; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final AtomicBoolean busy = new AtomicBoolean(false); + + /** + * Creates a new connection by first opening a plain TCP socket, + * sending the AMMC magic, waiting for the AMOK reply, and then upgrading to TLS. + */ + public Connection(InetSocketAddress remoteAddress, Secrets.Secret secret) throws Exception { + // Step 1. Create a plain TCP connection. + Socket plainSocket = new Socket(remoteAddress.getHostName(), remoteAddress.getPort()); + DataOutputStream plainOut = new DataOutputStream(plainSocket.getOutputStream()); + DataInputStream plainIn = new DataInputStream(plainSocket.getInputStream()); + + // Step 2. Send the handshake (AMMC magic) over the plain socket. + plainOut.writeInt(MAGIC_AMMC); + plainOut.flush(); + + // Step 3. Wait for the server’s reply (AMOK magic). + int handshakeResponse = plainIn.readInt(); + if (handshakeResponse != MAGIC_AMOK) { + plainSocket.close(); + throw new IOException("Invalid handshake response from server: " + handshakeResponse); + } + + // Step 4. Upgrade the plain socket to TLS using the same underlying connection. + SSLContext context = createSSLContext(); + SSLSocketFactory factory = context.getSocketFactory(); + // The createSocket(Socket, host, port, autoClose) wraps the existing plain socket. + SSLSocket sslSocket = (SSLSocket) factory.createSocket(plainSocket, remoteAddress.getHostName(), remoteAddress.getPort(), true); + sslSocket.setEnabledProtocols(new String[] {"TLSv1.3"}); + sslSocket.setEnabledCipherSuites(new String[] {"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"}); + sslSocket.startHandshake(); + + // Step 5. Perform custom TLS certificate validation. + Certificate[] certs = sslSocket.getSession().getPeerCertificates(); + if (certs == null || certs.length == 0 || certs.length > 3) { + sslSocket.close(); + throw new IOException("Invalid server certificate chain"); + } + boolean validated = false; + for (Certificate cert : certs) { + if (cert instanceof X509Certificate x509Cert) { + String fingerprint = NetUtils.getFingerprint(x509Cert, secret.secret()); + if (fingerprint.equals(secret.fingerprint())) { + validated = true; + break; + } + } + } + + if (!validated) { + sslSocket.close(); + throw new IOException("Server certificate validation failed"); + } + + secretBytes = Base64.getUrlDecoder().decode(secret.secret()); + + // Now use the SSL socket for further communication. + this.socket = sslSocket; + this.in = new DataInputStream(sslSocket.getInputStream()); + this.out = new DataOutputStream(sslSocket.getOutputStream()); + } + + public boolean isBusy() { + return busy.get(); + } + + public void setBusy(boolean value) { + busy.set(value); + } + + /** + * Sends a file request over this connection. + */ + public CompletableFuture sendDownloadFile(byte[] fileHash, Path destination) { + return CompletableFuture.supplyAsync(() -> { + try { + // Build File Request message: + // [protocolVersion][FILE_REQUEST_TYPE][secret][int: fileHash.length][fileHash] + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeByte(PROTOCOL_VERSION); + dos.writeByte(FILE_REQUEST_TYPE); + dos.write(secretBytes); + dos.writeInt(fileHash.length); + dos.write(fileHash); + dos.flush(); + byte[] payload = baos.toByteArray(); + + writeProtocolMessage(payload); + return readFileResponse(destination); + } catch (Exception e) { + throw new CompletionException(e); + } finally { + setBusy(false); + } + }, executor); + } + + /** + * Sends a refresh request over this connection. + */ + public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { + return CompletableFuture.supplyAsync(() -> { + try { + // Build Refresh Request message: + // [protocolVersion][REFRESH_REQUEST_TYPE][secret][int: fileHashesCount] + // [int: fileHashLength] then each file hash. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeByte(PROTOCOL_VERSION); + dos.writeByte(REFRESH_REQUEST_TYPE); + dos.write(secretBytes); + dos.writeInt(fileHashes.length); + if (fileHashes.length > 0) { + dos.writeInt(fileHashes[0].length); // assuming all hashes have same length + for (byte[] hash : fileHashes) { + dos.write(hash); + } + } + dos.flush(); + byte[] payload = baos.toByteArray(); + + writeProtocolMessage(payload); + return readFileResponse(null); + } catch (Exception e) { + throw new CompletionException(e); + } finally { + setBusy(false); + } + }, executor); + } + + /** + * Compresses and writes a protocol message using Zstd. + * Message framing: [int: compressedLength][int: originalLength][compressed payload]. + */ + private void writeProtocolMessage(byte[] payload) throws IOException { + byte[] compressed = Zstd.compress(payload); + out.writeInt(compressed.length); + out.writeInt(payload.length); + out.write(compressed); + out.flush(); + } + + /** + * Reads one framed protocol message, decompressing it. + */ + private byte[] readProtocolMessageFrame() throws IOException { + int compLength = in.readInt(); + int origLength = in.readInt(); + byte[] compData = new byte[compLength]; + in.readFully(compData); + return Zstd.decompress(compData, origLength); + } + + /** + * Processes a file/refresh response according to your protocol. + * The response is expected to have: + * - A header frame: [protocolVersion][messageType][(if FILE_RESPONSE_TYPE) long expectedFileSize] + * - One or more data frames containing file data until the total file size is reached. + * - A final frame: [protocolVersion][END_OF_TRANSMISSION] + */ + private Object readFileResponse(Path destination) throws IOException { + // Header frame + byte[] headerFrame = readProtocolMessageFrame(); + DataInputStream headerIn = new DataInputStream(new ByteArrayInputStream(headerFrame)); + byte version = headerIn.readByte(); + byte messageType = headerIn.readByte(); + if (messageType == ERROR) { + int errLen = headerIn.readInt(); + byte[] errBytes = new byte[errLen]; + headerIn.readFully(errBytes); + throw new IOException("Server error: " + new String(errBytes)); + } + if (messageType != FILE_RESPONSE_TYPE) { + throw new IOException("Unexpected message type: " + messageType); + } + long expectedFileSize = headerIn.readLong(); + + long receivedBytes = 0; + OutputStream fos = null; + List rawData = null; + if (destination != null) { + fos = new FileOutputStream(destination.toFile()); + } else { + rawData = new LinkedList<>(); + } + + // Read data frames until the expected file size is received. + while (receivedBytes < expectedFileSize) { + byte[] dataFrame = readProtocolMessageFrame(); + int toWrite = dataFrame.length; + if (receivedBytes + toWrite > expectedFileSize) { + toWrite = (int)(expectedFileSize - receivedBytes); + } + if (fos != null) { + fos.write(dataFrame, 0, toWrite); + } else { + byte[] chunk = new byte[toWrite]; + System.arraycopy(dataFrame, 0, chunk, 0, toWrite); + rawData.add(chunk); + } + receivedBytes += toWrite; + } + + // Read EOT frame + byte[] eotFrame = readProtocolMessageFrame(); + DataInputStream eotIn = new DataInputStream(new ByteArrayInputStream(eotFrame)); + byte ver = eotIn.readByte(); + byte eotType = eotIn.readByte(); + if (ver != version || eotType != END_OF_TRANSMISSION) { + throw new IOException("Invalid end-of-transmission marker. Expected version " + version + + " and type " + END_OF_TRANSMISSION + ", got version " + ver + " and type " + eotType); + } + + if (fos != null) { + fos.close(); + return destination; + } else { + return rawData; + } + } + + /** + * Closes the underlying socket and shuts down the executor. + */ + public void close() { + try { + socket.close(); + } catch (Exception e) { + // Log or handle as needed. + } + executor.shutdownNow(); + } + + /** + * Creates an SSLContext that trusts all certificates (like InsecureTrustManagerFactory). + */ + private SSLContext createSSLContext() throws Exception { + SSLContext sslContext = SSLContext.getInstance("TLSv1.3"); + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + public void checkClientTrusted(X509Certificate[] certs, String authType) { } + public void checkServerTrusted(X509Certificate[] certs, String authType) { } + } + }; + sslContext.init(null, trustAllCerts, new SecureRandom()); + return sslContext; + } +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java b/core/src/main/java/pl/skidam/protocol/NetUtils.java similarity index 99% rename from core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java rename to core/src/main/java/pl/skidam/protocol/NetUtils.java index 8ff7a744..9e97badb 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/NetUtils.java +++ b/core/src/main/java/pl/skidam/protocol/NetUtils.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty; +package pl.skidam.protocol; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/NettyServer.java b/core/src/main/java/pl/skidam/protocol/netty/NettyServer.java similarity index 98% rename from core/src/main/java/pl/skidam/automodpack_core/netty/NettyServer.java rename to core/src/main/java/pl/skidam/protocol/netty/NettyServer.java index 14f75d33..59348d4f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/protocol/netty/NettyServer.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty; +package pl.skidam.protocol.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; @@ -12,7 +12,8 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.automodpack_core.netty.handler.ProtocolServerHandler; +import pl.skidam.protocol.NetUtils; +import pl.skidam.protocol.netty.handler.ProtocolServerHandler; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; import pl.skidam.automodpack_core.utils.Ip; import pl.skidam.automodpack_core.utils.ObservableMap; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java b/core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java similarity index 95% rename from core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java rename to core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java index c748df71..32bf8422 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/client/DownloadClient.java +++ b/core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.client; +package pl.skidam.protocol.netty.client; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; @@ -11,9 +11,9 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.stream.ChunkedWriteHandler; import pl.skidam.automodpack_core.auth.Secrets; -import pl.skidam.automodpack_core.netty.handler.*; -import pl.skidam.automodpack_core.netty.message.FileRequestMessage; -import pl.skidam.automodpack_core.netty.message.RefreshRequestMessage; +import pl.skidam.protocol.netty.handler.*; +import pl.skidam.protocol.netty.message.FileRequestMessage; +import pl.skidam.protocol.netty.message.RefreshRequestMessage; import javax.net.ssl.SSLException; import java.net.InetSocketAddress; @@ -24,7 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; -import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMMC; +import static pl.skidam.protocol.NetUtils.MAGIC_AMMC; public class DownloadClient extends NettyClient { private final Map channels = new HashMap<>(); // channel, isBusy diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java b/core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java similarity index 92% rename from core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java rename to core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java index 83823aa3..151913f5 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/client/EchoClient.java +++ b/core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.client; +package pl.skidam.protocol.netty.client; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; @@ -10,9 +10,9 @@ import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import pl.skidam.automodpack_core.auth.Secrets; -import pl.skidam.automodpack_core.netty.handler.ProtocolClientHandler; -import pl.skidam.automodpack_core.netty.handler.ProtocolMessageEncoder; -import pl.skidam.automodpack_core.netty.message.EchoMessage; +import pl.skidam.protocol.netty.handler.ProtocolClientHandler; +import pl.skidam.protocol.netty.handler.ProtocolMessageEncoder; +import pl.skidam.protocol.netty.message.EchoMessage; import javax.net.ssl.SSLException; import java.net.InetSocketAddress; @@ -22,7 +22,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Semaphore; -import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMMC; +import static pl.skidam.protocol.NetUtils.MAGIC_AMMC; public class EchoClient extends NettyClient { private final List channels = new ArrayList<>(); diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java b/core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java similarity index 91% rename from core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java rename to core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java index bca2f2ff..2b2cadd0 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/client/NettyClient.java +++ b/core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.client; +package pl.skidam.protocol.netty.client; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java b/core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java similarity index 98% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java index 8b63271c..e12a6acb 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/FileDownloadHandler.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -13,7 +13,7 @@ import java.util.concurrent.CompletableFuture; import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; -import static pl.skidam.automodpack_core.netty.NetUtils.*; +import static pl.skidam.protocol.NetUtils.*; public class FileDownloadHandler extends ChannelInboundHandlerAdapter { diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java similarity index 93% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java index a0dbc34f..40063f77 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolClientHandler.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java @@ -1,17 +1,17 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.ssl.SslHandler; -import pl.skidam.automodpack_core.netty.NetUtils; -import pl.skidam.automodpack_core.netty.client.NettyClient; +import pl.skidam.protocol.NetUtils; +import pl.skidam.protocol.netty.client.NettyClient; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.List; -import static pl.skidam.automodpack_core.netty.NetUtils.MAGIC_AMOK; +import static pl.skidam.protocol.NetUtils.MAGIC_AMOK; public class ProtocolClientHandler extends ByteToMessageDecoder { diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageDecoder.java similarity index 83% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageDecoder.java index e5a38e01..c8fdbe63 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageDecoder.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageDecoder.java @@ -1,17 +1,17 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; -import pl.skidam.automodpack_core.netty.NetUtils; -import pl.skidam.automodpack_core.netty.message.EchoMessage; -import pl.skidam.automodpack_core.netty.message.FileRequestMessage; -import pl.skidam.automodpack_core.netty.message.FileResponseMessage; -import pl.skidam.automodpack_core.netty.message.RefreshRequestMessage; +import pl.skidam.protocol.NetUtils; +import pl.skidam.protocol.netty.message.EchoMessage; +import pl.skidam.protocol.netty.message.FileRequestMessage; +import pl.skidam.protocol.netty.message.FileResponseMessage; +import pl.skidam.protocol.netty.message.RefreshRequestMessage; import java.util.List; -import static pl.skidam.automodpack_core.netty.NetUtils.*; +import static pl.skidam.protocol.NetUtils.*; public class ProtocolMessageDecoder extends ByteToMessageDecoder { @Override diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageEncoder.java similarity index 92% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageEncoder.java index 6531a86c..b05cf0b4 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolMessageEncoder.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageEncoder.java @@ -1,11 +1,11 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import pl.skidam.automodpack_core.netty.message.*; +import pl.skidam.protocol.netty.message.*; -import static pl.skidam.automodpack_core.netty.NetUtils.*; +import static pl.skidam.protocol.NetUtils.*; public class ProtocolMessageEncoder extends MessageToByteEncoder { @Override diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolServerHandler.java similarity index 95% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolServerHandler.java index 4ebfc364..e915362f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ProtocolServerHandler.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolServerHandler.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -8,7 +8,7 @@ import java.util.List; -import static pl.skidam.automodpack_core.netty.NetUtils.*; +import static pl.skidam.protocol.NetUtils.*; public class ProtocolServerHandler extends ByteToMessageDecoder { diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ServerMessageHandler.java similarity index 94% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ServerMessageHandler.java index f864b296..506c857a 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ServerMessageHandler.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ServerMessageHandler.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -10,10 +10,10 @@ import pl.skidam.automodpack_core.GlobalVariables; import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.modpack.ModpackContent; -import pl.skidam.automodpack_core.netty.message.EchoMessage; -import pl.skidam.automodpack_core.netty.message.FileRequestMessage; -import pl.skidam.automodpack_core.netty.message.ProtocolMessage; -import pl.skidam.automodpack_core.netty.message.RefreshRequestMessage; +import pl.skidam.protocol.netty.message.EchoMessage; +import pl.skidam.protocol.netty.message.FileRequestMessage; +import pl.skidam.protocol.netty.message.ProtocolMessage; +import pl.skidam.protocol.netty.message.RefreshRequestMessage; import java.io.File; import java.io.IOException; @@ -25,7 +25,7 @@ import java.util.concurrent.CompletableFuture; import static pl.skidam.automodpack_core.GlobalVariables.*; -import static pl.skidam.automodpack_core.netty.NetUtils.*; +import static pl.skidam.protocol.NetUtils.*; public class ServerMessageHandler extends SimpleChannelInboundHandler { @@ -136,10 +136,10 @@ private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) { responseHeader.writeLong(file.length()); ctx.writeAndFlush(responseHeader); - // Stream the file using ChunkedFile (chunk size set to 8192 bytes) + // Stream the file using ChunkedFile (chunk size set to 131072 bytes = 128 KB) - suitable value for zstd try { RandomAccessFile raf = new RandomAccessFile(file, "r"); - ChunkedFile chunkedFile = new ChunkedFile(raf, 0, raf.length(), 8192); + ChunkedFile chunkedFile = new ChunkedFile(raf, 0, raf.length(), 131072); ctx.writeAndFlush(chunkedFile).addListener((ChannelFutureListener) future -> { // After the file is sent, send an End-of-Transmission message. ByteBuf eot = Unpooled.buffer(2); diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdDecoder.java similarity index 86% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ZstdDecoder.java index 9f05e100..72d7806e 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdDecoder.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdDecoder.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import com.github.luben.zstd.Zstd; import io.netty.buffer.ByteBuf; @@ -7,8 +7,6 @@ import java.util.List; -import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; - public class ZstdDecoder extends ByteToMessageDecoder { @Override @@ -28,7 +26,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t byte[] compressed = new byte[length]; in.readBytes(compressed); - var time = System.currentTimeMillis(); +// var time = System.currentTimeMillis(); byte[] decompressed = Zstd.decompress(compressed, originalLength); // LOGGER.info("Decompression time: {}ms. Saved {} bytes", System.currentTimeMillis() - time, originalLength - length); diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdEncoder.java similarity index 81% rename from core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java rename to core/src/main/java/pl/skidam/protocol/netty/handler/ZstdEncoder.java index 4d2d44f8..f8181a7a 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/handler/ZstdEncoder.java +++ b/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdEncoder.java @@ -1,12 +1,10 @@ -package pl.skidam.automodpack_core.netty.handler; +package pl.skidam.protocol.netty.handler; import com.github.luben.zstd.Zstd; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; - public class ZstdEncoder extends MessageToByteEncoder { @Override @@ -14,7 +12,7 @@ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throw byte[] input = new byte[msg.readableBytes()]; msg.readBytes(input); - var time = System.currentTimeMillis(); +// var time = System.currentTimeMillis(); byte[] compressed = Zstd.compress(input); // LOGGER.info("Compression time: {}ms. Saved {} bytes", System.currentTimeMillis() - time, input.length - compressed.length); diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java b/core/src/main/java/pl/skidam/protocol/netty/message/EchoMessage.java similarity index 78% rename from core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java rename to core/src/main/java/pl/skidam/protocol/netty/message/EchoMessage.java index a4d1a5a8..52c410b6 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/message/EchoMessage.java +++ b/core/src/main/java/pl/skidam/protocol/netty/message/EchoMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.automodpack_core.netty.message; +package pl.skidam.protocol.netty.message; -import static pl.skidam.automodpack_core.netty.NetUtils.ECHO_TYPE; +import static pl.skidam.protocol.NetUtils.ECHO_TYPE; public class EchoMessage extends ProtocolMessage { private final int dataLength; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java b/core/src/main/java/pl/skidam/protocol/netty/message/FileRequestMessage.java similarity index 79% rename from core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java rename to core/src/main/java/pl/skidam/protocol/netty/message/FileRequestMessage.java index 8d19ceea..69cf3f82 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileRequestMessage.java +++ b/core/src/main/java/pl/skidam/protocol/netty/message/FileRequestMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.automodpack_core.netty.message; +package pl.skidam.protocol.netty.message; -import static pl.skidam.automodpack_core.netty.NetUtils.FILE_REQUEST_TYPE; +import static pl.skidam.protocol.NetUtils.FILE_REQUEST_TYPE; public class FileRequestMessage extends ProtocolMessage { private final int fileHashLength; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java b/core/src/main/java/pl/skidam/protocol/netty/message/FileResponseMessage.java similarity index 78% rename from core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java rename to core/src/main/java/pl/skidam/protocol/netty/message/FileResponseMessage.java index c4f81445..d33ddecf 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/message/FileResponseMessage.java +++ b/core/src/main/java/pl/skidam/protocol/netty/message/FileResponseMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.automodpack_core.netty.message; +package pl.skidam.protocol.netty.message; -import static pl.skidam.automodpack_core.netty.NetUtils.FILE_RESPONSE_TYPE; +import static pl.skidam.protocol.NetUtils.FILE_RESPONSE_TYPE; public class FileResponseMessage extends ProtocolMessage { private final int dataLength; diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java b/core/src/main/java/pl/skidam/protocol/netty/message/ProtocolMessage.java similarity index 92% rename from core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java rename to core/src/main/java/pl/skidam/protocol/netty/message/ProtocolMessage.java index 8f6720e0..1385a6f4 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/message/ProtocolMessage.java +++ b/core/src/main/java/pl/skidam/protocol/netty/message/ProtocolMessage.java @@ -1,4 +1,4 @@ -package pl.skidam.automodpack_core.netty.message; +package pl.skidam.protocol.netty.message; public abstract class ProtocolMessage { private final byte version; // 1 byte diff --git a/core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java b/core/src/main/java/pl/skidam/protocol/netty/message/RefreshRequestMessage.java similarity index 85% rename from core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java rename to core/src/main/java/pl/skidam/protocol/netty/message/RefreshRequestMessage.java index 1722da44..7aec6287 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/netty/message/RefreshRequestMessage.java +++ b/core/src/main/java/pl/skidam/protocol/netty/message/RefreshRequestMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.automodpack_core.netty.message; +package pl.skidam.protocol.netty.message; -import static pl.skidam.automodpack_core.netty.NetUtils.REFRESH_REQUEST_TYPE; +import static pl.skidam.protocol.NetUtils.REFRESH_REQUEST_TYPE; public class RefreshRequestMessage extends ProtocolMessage { private final int fileHashesCount; diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 9edf40f8..04fcac7b 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -3,7 +3,7 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.automodpack_core.netty.client.DownloadClient; +import pl.skidam.protocol.DownloadClient; import pl.skidam.automodpack_core.utils.*; import pl.skidam.automodpack_loader_core.ReLauncher; import pl.skidam.automodpack_loader_core.screen.ScreenManager; diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 02e51d9d..4401c013 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -6,7 +6,7 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.automodpack_core.netty.client.DownloadClient; +import pl.skidam.protocol.DownloadClient; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.FileInspection; import pl.skidam.automodpack_core.utils.ModpackContentTools; @@ -307,7 +307,7 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m InetSocketAddress selectedModpackAddress = null; try { int portIndex = selectedModpackLink.lastIndexOf(":"); - selectedModpackAddress = new InetSocketAddress(selectedModpackLink.substring(0, portIndex), Integer.parseInt(selectedModpackLink.substring(portIndex + 1))); + if (portIndex != -1) selectedModpackAddress = new InetSocketAddress(selectedModpackLink.substring(0, portIndex), Integer.parseInt(selectedModpackLink.substring(portIndex + 1))); } catch (Exception e) { if (selectedModpackLink != null && !selectedModpackLink.isBlank()) { LOGGER.error("Error while parsing selected modpack address", e); @@ -528,7 +528,7 @@ public static Optional parseStreamToModpack(List parseStreamToModpack(InputStr String response = null; try (InputStreamReader isr = new InputStreamReader(stream)) { - JsonElement element = new JsonParser().parse(isr); // Needed to parse by deprecated method because of older minecraft versions (<1.17.1) + JsonElement element = JsonParser.parseReader(isr); // Needed to parse by deprecated method because of older minecraft versions (<1.17.1) if (element != null && !element.isJsonArray()) { JsonObject obj = element.getAsJsonObject(); response = obj.toString(); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 99dda10e..38b61ece 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -1,9 +1,9 @@ package pl.skidam.automodpack_loader_core.utils; -import pl.skidam.automodpack_core.netty.client.DownloadClient; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; import pl.skidam.automodpack_core.utils.FileInspection; +import pl.skidam.protocol.DownloadClient; import java.io.*; import java.net.*; @@ -53,14 +53,14 @@ private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownlo int numberOfIndexes = queuedDownload.urls.size(); int urlIndex = Math.min(queuedDownload.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes); - String url = null; + String url = "host"; if (queuedDownload.urls.size() > urlIndex) { // avoids IndexOutOfBoundsException url = queuedDownload.urls.get(urlIndex); } boolean interrupted = false; try { - if (url != null && queuedDownload.attempts < MAX_DOWNLOAD_ATTEMPTS * numberOfIndexes) { + if (url != null && !Objects.equals(url, "host") && queuedDownload.attempts < MAX_DOWNLOAD_ATTEMPTS * numberOfIndexes) { httpDownloadFile(url, hashPathPair, queuedDownload); } else if (downloadClient != null) { hostDownloadFile(hashPathPair, queuedDownload); diff --git a/src/main/java/pl/skidam/automodpack/init/Common.java b/src/main/java/pl/skidam/automodpack/init/Common.java index 3de2a3d4..d8147ce1 100644 --- a/src/main/java/pl/skidam/automodpack/init/Common.java +++ b/src/main/java/pl/skidam/automodpack/init/Common.java @@ -6,7 +6,7 @@ import pl.skidam.automodpack.networking.ModPackets; import pl.skidam.automodpack_core.modpack.Modpack; import pl.skidam.automodpack_core.loader.LoaderManagerService; -import pl.skidam.automodpack_core.netty.NettyServer; +import pl.skidam.protocol.netty.NettyServer; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/pl/skidam/automodpack/loader/GameCall.java b/src/main/java/pl/skidam/automodpack/loader/GameCall.java index f67047f4..2bfc27d5 100644 --- a/src/main/java/pl/skidam/automodpack/loader/GameCall.java +++ b/src/main/java/pl/skidam/automodpack/loader/GameCall.java @@ -2,13 +2,13 @@ import com.mojang.authlib.GameProfile; import net.minecraft.util.UserCache; -import pl.skidam.automodpack.init.Common; import pl.skidam.automodpack.modpack.GameHelpers; import pl.skidam.automodpack_core.loader.GameCallService; import java.net.SocketAddress; import java.util.UUID; +import static pl.skidam.automodpack.init.Common.server; import static pl.skidam.automodpack_core.GlobalVariables.*; public class GameCall implements GameCallService { @@ -19,12 +19,12 @@ public boolean canPlayerJoin(SocketAddress address, String id) { String playerName = "Player"; // mock name, name matters less than UUID anyway GameProfile profile = new GameProfile(uuid, playerName); - UserCache userCache = Common.server.getUserCache(); + UserCache userCache = server.getUserCache(); if (userCache != null) { profile = userCache.getByUuid(uuid).orElse(profile); } - if (Common.server == null) { + if (server == null) { LOGGER.error("Server is null?"); return true; } diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java index 88b21ac5..a3103374 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import pl.skidam.automodpack_core.GlobalVariables; -import pl.skidam.automodpack_core.netty.handler.ProtocolServerHandler; +import pl.skidam.protocol.netty.handler.ProtocolServerHandler; import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; import static pl.skidam.automodpack_core.GlobalVariables.hostServer; From 2f020f02f6477c30dc35968efdb4ee4ec9d58d8f Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 24 Feb 2025 13:55:07 +0100 Subject: [PATCH 13/50] Fix NumberFormatException with old config --- .../skidam/automodpack_loader_core/client/ModpackUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 4401c013..1fbf60db 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -306,8 +306,10 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m InetSocketAddress selectedModpackAddress = null; try { - int portIndex = selectedModpackLink.lastIndexOf(":"); - if (portIndex != -1) selectedModpackAddress = new InetSocketAddress(selectedModpackLink.substring(0, portIndex), Integer.parseInt(selectedModpackLink.substring(portIndex + 1))); + String[] parts = selectedModpackLink.split(":"); + if (parts.length == 2 && parts[1].matches("\\d+")) { + selectedModpackAddress = new InetSocketAddress(parts[0], Integer.parseInt(parts[1])); + } } catch (Exception e) { if (selectedModpackLink != null && !selectedModpackLink.isBlank()) { LOGGER.error("Error while parsing selected modpack address", e); From 4ea8ec646a4741a7fac742fc662faac68d6e4028 Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 24 Feb 2025 15:09:36 +0100 Subject: [PATCH 14/50] Fix in game download progress bar --- .../automodpack_core/callbacks/IntCallback.java | 5 +++++ .../java/pl/skidam/protocol/DownloadClient.java | 17 +++++++++++------ .../client/ModpackUtils.java | 14 ++++++++++---- .../utils/DownloadManager.java | 5 ++++- 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/pl/skidam/automodpack_core/callbacks/IntCallback.java diff --git a/core/src/main/java/pl/skidam/automodpack_core/callbacks/IntCallback.java b/core/src/main/java/pl/skidam/automodpack_core/callbacks/IntCallback.java new file mode 100644 index 00000000..6ed20d28 --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/callbacks/IntCallback.java @@ -0,0 +1,5 @@ +package pl.skidam.automodpack_core.callbacks; + +public interface IntCallback { + void run(int bytes); +} diff --git a/core/src/main/java/pl/skidam/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/protocol/DownloadClient.java index 93c74618..b67b0ef2 100644 --- a/core/src/main/java/pl/skidam/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/protocol/DownloadClient.java @@ -2,6 +2,7 @@ import pl.skidam.automodpack_core.auth.Secrets; import com.github.luben.zstd.Zstd; +import pl.skidam.automodpack_core.callbacks.IntCallback; import javax.net.ssl.*; import java.io.*; @@ -49,9 +50,9 @@ private synchronized Connection getFreeConnection() { * Downloads a file identified by its SHA-1 hash to the given destination. * Returns a CompletableFuture that completes when the download finishes. */ - public CompletableFuture downloadFile(byte[] fileHash, Path destination) { + public CompletableFuture downloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { Connection conn = getFreeConnection(); - return conn.sendDownloadFile(fileHash, destination); + return conn.sendDownloadFile(fileHash, destination, chunkCallback); } /** @@ -158,7 +159,7 @@ public void setBusy(boolean value) { /** * Sends a file request over this connection. */ - public CompletableFuture sendDownloadFile(byte[] fileHash, Path destination) { + public CompletableFuture sendDownloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { return CompletableFuture.supplyAsync(() -> { try { // Build File Request message: @@ -174,7 +175,7 @@ public CompletableFuture sendDownloadFile(byte[] fileHash, Path destinat byte[] payload = baos.toByteArray(); writeProtocolMessage(payload); - return readFileResponse(destination); + return readFileResponse(destination, chunkCallback); } catch (Exception e) { throw new CompletionException(e); } finally { @@ -208,7 +209,7 @@ public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { byte[] payload = baos.toByteArray(); writeProtocolMessage(payload); - return readFileResponse(null); + return readFileResponse(null, null); } catch (Exception e) { throw new CompletionException(e); } finally { @@ -247,7 +248,7 @@ private byte[] readProtocolMessageFrame() throws IOException { * - One or more data frames containing file data until the total file size is reached. * - A final frame: [protocolVersion][END_OF_TRANSMISSION] */ - private Object readFileResponse(Path destination) throws IOException { + private Object readFileResponse(Path destination, IntCallback chunkCallback) throws IOException { // Header frame byte[] headerFrame = readProtocolMessageFrame(); DataInputStream headerIn = new DataInputStream(new ByteArrayInputStream(headerFrame)); @@ -288,6 +289,10 @@ private Object readFileResponse(Path destination) throws IOException { rawData.add(chunk); } receivedBytes += toWrite; + + if (chunkCallback != null) { + chunkCallback.run(toWrite); + } } // Read EOT frame diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 1fbf60db..6758869f 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -306,9 +306,15 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m InetSocketAddress selectedModpackAddress = null; try { - String[] parts = selectedModpackLink.split(":"); - if (parts.length == 2 && parts[1].matches("\\d+")) { - selectedModpackAddress = new InetSocketAddress(parts[0], Integer.parseInt(parts[1])); + int portIndex = selectedModpackLink.lastIndexOf(':'); + if (portIndex != -1) { + String host = selectedModpackLink.substring(0, portIndex); + String port = selectedModpackLink.substring(portIndex + 1); + if (port.matches("\\d+")) { + selectedModpackAddress = new InetSocketAddress(host, Integer.parseInt(port)); + } + } else { + selectedModpackAddress = new InetSocketAddress(selectedModpackLink, 0); } } catch (Exception e) { if (selectedModpackLink != null && !selectedModpackLink.isBlank()) { @@ -411,7 +417,7 @@ public static Optional requestServerModpackContent(I DownloadClient client = null; try { client = new DownloadClient(address, secret, 1); - var future = client.downloadFile(new byte[0], null); + var future = client.downloadFile(new byte[0], null, null); var result = future.get(); if (result instanceof List list) { return parseStreamToModpack((List) list); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 38b61ece..84b31f9a 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -162,7 +162,10 @@ private void hostDownloadFile(FileInspection.HashPathPair hashPathPair, QueuedDo // Files.createFile(outFile); } - var future = downloadClient.downloadFile(hashPathPair.hash().getBytes(StandardCharsets.UTF_8), outFile); + var future = downloadClient.downloadFile(hashPathPair.hash().getBytes(StandardCharsets.UTF_8), outFile, (bytes) -> { + bytesDownloaded += bytes; + speedMeter.addDownloadedBytes(bytes); + }); future.join(); } From 1f78a717fa1a38778705e8e0eb1d79f3f5e62318 Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 24 Feb 2025 18:44:27 +0100 Subject: [PATCH 15/50] Whoopsie ref: #324 --- .../java/pl/skidam/automodpack/modpack/GameHelpers.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java b/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java index 52cad93f..f0a29a78 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java +++ b/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java @@ -13,9 +13,11 @@ public static boolean isPlayerAuthorized(SocketAddress address, GameProfile prof var playerManager = server.getPlayerManager(); if (playerManager.getUserBanList().contains(profile)) { return false; - } else if (!playerManager.isWhitelisted(profile)) { + } + if (!playerManager.isWhitelisted(profile)) { return false; - } else if (playerManager.getIpBanList().isBanned(address)) { + } + if (playerManager.getIpBanList().isBanned(address)) { return false; } From 04b26fd8fc76711db91d89742154fc8f78a3c59a Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 10:02:05 +0100 Subject: [PATCH 16/50] Improvements, mainly DownloadClient stuff ref: #319 --- core/build.gradle.kts | 2 +- .../automodpack_core/GlobalVariables.java | 3 +- .../skidam/automodpack_core/auth/Secrets.java | 4 +- .../loader/GameCallService.java | 2 +- .../automodpack_core/loader/NullGameCall.java | 2 +- .../protocol/DownloadClient.java | 141 ++++++++------ .../protocol/NetUtils.java | 2 +- .../protocol/netty/NettyServer.java | 6 +- .../netty/handler/ProtocolMessageDecoder.java | 14 +- .../netty/handler/ProtocolMessageEncoder.java | 6 +- .../netty/handler/ProtocolServerHandler.java | 4 +- .../netty/handler/ServerMessageHandler.java | 44 +++-- .../protocol/netty/handler/ZstdDecoder.java | 2 +- .../protocol/netty/handler/ZstdEncoder.java | 2 +- .../protocol/netty/message/EchoMessage.java | 4 +- .../netty/message/FileRequestMessage.java | 4 +- .../netty/message/FileResponseMessage.java | 4 +- .../netty/message/ProtocolMessage.java | 2 +- .../netty/message/RefreshRequestMessage.java | 4 +- .../protocol/netty/client/DownloadClient.java | 183 ------------------ .../protocol/netty/client/EchoClient.java | 125 ------------ .../protocol/netty/client/NettyClient.java | 15 -- .../netty/handler/FileDownloadHandler.java | 154 --------------- .../netty/handler/ProtocolClientHandler.java | 96 --------- .../client/ModpackUpdater.java | 4 +- .../client/ModpackUtils.java | 101 +--------- .../utils/DownloadManager.java | 28 +-- loader/loader-fabric-core.gradle.kts | 14 +- loader/loader-forge.gradle.kts | 11 +- .../pl/skidam/automodpack/init/Common.java | 2 +- .../skidam/automodpack/loader/GameCall.java | 2 +- .../mixin/core/ServerNetworkIoMixin.java | 2 +- 32 files changed, 167 insertions(+), 822 deletions(-) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/DownloadClient.java (76%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/NetUtils.java (99%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/NettyServer.java (97%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/handler/ProtocolMessageDecoder.java (82%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/handler/ProtocolMessageEncoder.java (91%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/handler/ProtocolServerHandler.java (94%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/handler/ServerMessageHandler.java (84%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/handler/ZstdDecoder.java (95%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/handler/ZstdEncoder.java (93%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/message/EchoMessage.java (76%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/message/FileRequestMessage.java (78%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/message/FileResponseMessage.java (76%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/message/ProtocolMessage.java (91%) rename core/src/main/java/pl/skidam/{ => automodpack_core}/protocol/netty/message/RefreshRequestMessage.java (83%) delete mode 100644 core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java delete mode 100644 core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java delete mode 100644 core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java delete mode 100644 core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java delete mode 100644 core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a9e6c66a..26178445 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation("org.apache.logging.log4j:log4j-core:2.20.0") implementation("com.google.code.gson:gson:2.10.1") implementation("io.netty:netty-all:4.1.118.Final") - implementation("org.bouncycastle:bcprov-jdk18on:1.80") +// implementation("org.bouncycastle:bcprov-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("com.github.luben:zstd-jni:1.5.7-1") implementation("org.tomlj:tomlj:1.1.1") diff --git a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java index 904df9ed..0a381797 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java +++ b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java @@ -5,14 +5,13 @@ import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.loader.*; import pl.skidam.automodpack_core.modpack.Modpack; -import pl.skidam.protocol.netty.NettyServer; +import pl.skidam.automodpack_core.protocol.netty.NettyServer; import java.nio.file.Path; public class GlobalVariables { public static final Logger LOGGER = LogManager.getLogger("AutoModpack"); public static final String MOD_ID = "automodpack"; - public static final String SECRET_REQUEST_HEADER = "AutoModpack-Secret"; public static Boolean DEBUG = false; public static Boolean preload; public static String MC_VERSION; diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java index 2ab68dfd..2bf9d9a4 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java @@ -1,6 +1,6 @@ package pl.skidam.automodpack_core.auth; -import pl.skidam.protocol.NetUtils; +import pl.skidam.automodpack_core.protocol.NetUtils; import java.net.SocketAddress; import java.security.SecureRandom; @@ -51,7 +51,7 @@ public static boolean isSecretValid(String secretStr, SocketAddress address) { return false; String playerUuid = playerSecretPair.getKey(); - if (!GAME_CALL.canPlayerJoin(address, playerUuid)) // check if associated player is still whitelisted + if (!GAME_CALL.isPlayerAuthorized(address, playerUuid)) // check if associated player is still whitelisted return false; long secretLifetime = serverConfig.secretLifetime * 3600; // in seconds diff --git a/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java b/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java index 34ee0232..2b6863b9 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java +++ b/core/src/main/java/pl/skidam/automodpack_core/loader/GameCallService.java @@ -3,5 +3,5 @@ import java.net.SocketAddress; public interface GameCallService { - boolean canPlayerJoin(SocketAddress address, String id); + boolean isPlayerAuthorized(SocketAddress address, String id); } \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java b/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java index e9cb9363..d30f1673 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java +++ b/core/src/main/java/pl/skidam/automodpack_core/loader/NullGameCall.java @@ -4,7 +4,7 @@ public class NullGameCall implements GameCallService { @Override - public boolean canPlayerJoin(SocketAddress address, String id) { + public boolean isPlayerAuthorized(SocketAddress address, String id) { return true; } } diff --git a/core/src/main/java/pl/skidam/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java similarity index 76% rename from core/src/main/java/pl/skidam/protocol/DownloadClient.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index b67b0ef2..51510a1d 100644 --- a/core/src/main/java/pl/skidam/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol; +package pl.skidam.automodpack_core.protocol; import pl.skidam.automodpack_core.auth.Secrets; import com.github.luben.zstd.Zstd; @@ -12,14 +12,11 @@ import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Base64; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; -import static pl.skidam.protocol.NetUtils.*; +import static pl.skidam.automodpack_core.protocol.NetUtils.*; /** * A DownloadClient that creates a pool of connections. @@ -124,6 +121,7 @@ public Connection(InetSocketAddress remoteAddress, Secrets.Secret secret) throws sslSocket.close(); throw new IOException("Invalid server certificate chain"); } + boolean validated = false; for (Certificate cert : certs) { if (cert instanceof X509Certificate x509Cert) { @@ -161,6 +159,7 @@ public void setBusy(boolean value) { */ public CompletableFuture sendDownloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { return CompletableFuture.supplyAsync(() -> { + Exception exception = null; try { // Build File Request message: // [protocolVersion][FILE_REQUEST_TYPE][secret][int: fileHash.length][fileHash] @@ -177,9 +176,10 @@ public CompletableFuture sendDownloadFile(byte[] fileHash, Path destinat writeProtocolMessage(payload); return readFileResponse(destination, chunkCallback); } catch (Exception e) { + exception = e; throw new CompletionException(e); } finally { - setBusy(false); + finalBlock(exception); } }, executor); } @@ -189,6 +189,7 @@ public CompletableFuture sendDownloadFile(byte[] fileHash, Path destinat */ public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { return CompletableFuture.supplyAsync(() -> { + Exception exception = null; try { // Build Refresh Request message: // [protocolVersion][REFRESH_REQUEST_TYPE][secret][int: fileHashesCount] @@ -211,13 +212,32 @@ public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { writeProtocolMessage(payload); return readFileResponse(null, null); } catch (Exception e) { + exception = e; throw new CompletionException(e); } finally { - setBusy(false); + finalBlock(exception); } }, executor); } + private void finalBlock(Exception exception) { + // skip any remaining data + try { + while (in.available() > 0) { + in.skipBytes(in.available()); + } + } catch (IOException e) { + if (exception == null) { + exception = e; + throw new CompletionException(e); + } + } finally { + if (exception == null) { + setBusy(false); + } + } + } + /** * Compresses and writes a protocol message using Zstd. * Message framing: [int: compressedLength][int: originalLength][compressed payload]. @@ -251,65 +271,66 @@ private byte[] readProtocolMessageFrame() throws IOException { private Object readFileResponse(Path destination, IntCallback chunkCallback) throws IOException { // Header frame byte[] headerFrame = readProtocolMessageFrame(); - DataInputStream headerIn = new DataInputStream(new ByteArrayInputStream(headerFrame)); - byte version = headerIn.readByte(); - byte messageType = headerIn.readByte(); - if (messageType == ERROR) { - int errLen = headerIn.readInt(); - byte[] errBytes = new byte[errLen]; - headerIn.readFully(errBytes); - throw new IOException("Server error: " + new String(errBytes)); - } - if (messageType != FILE_RESPONSE_TYPE) { - throw new IOException("Unexpected message type: " + messageType); - } - long expectedFileSize = headerIn.readLong(); - - long receivedBytes = 0; - OutputStream fos = null; - List rawData = null; - if (destination != null) { - fos = new FileOutputStream(destination.toFile()); - } else { - rawData = new LinkedList<>(); - } + try (DataInputStream headerIn = new DataInputStream(new ByteArrayInputStream(headerFrame))) { + byte version = headerIn.readByte(); + byte messageType = headerIn.readByte(); + + if (messageType == ERROR) { + int errLen = headerIn.readInt(); + byte[] errBytes = new byte[errLen]; + headerIn.readFully(errBytes); + throw new IOException("Server error: " + new String(errBytes)); + } + + long receivedBytes = 0; + OutputStream fos = (destination != null) ? new FileOutputStream(destination.toFile()) : null; + List rawData = (fos == null) ? new LinkedList<>() : null; - // Read data frames until the expected file size is received. - while (receivedBytes < expectedFileSize) { - byte[] dataFrame = readProtocolMessageFrame(); - int toWrite = dataFrame.length; - if (receivedBytes + toWrite > expectedFileSize) { - toWrite = (int)(expectedFileSize - receivedBytes); + if (messageType == END_OF_TRANSMISSION) { + if (fos != null) fos.close(); + return (rawData != null) ? rawData : destination; } - if (fos != null) { - fos.write(dataFrame, 0, toWrite); - } else { - byte[] chunk = new byte[toWrite]; - System.arraycopy(dataFrame, 0, chunk, 0, toWrite); - rawData.add(chunk); + + if (messageType != FILE_RESPONSE_TYPE) { + if (fos != null) fos.close(); + throw new IOException("Unexpected message type: " + messageType); } - receivedBytes += toWrite; - if (chunkCallback != null) { - chunkCallback.run(toWrite); + long expectedFileSize = headerIn.readLong(); + + // Read data frames until the expected file size is received. + while (receivedBytes < expectedFileSize) { + byte[] dataFrame = readProtocolMessageFrame(); + int toWrite = Math.min(dataFrame.length, (int)(expectedFileSize - receivedBytes)); + + if (fos != null) { + fos.write(dataFrame, 0, toWrite); + } else { + byte[] chunk = Arrays.copyOfRange(dataFrame, 0, toWrite); + rawData.add(chunk); + } + receivedBytes += toWrite; + + if (chunkCallback != null) { + chunkCallback.run(toWrite); + } } - } - // Read EOT frame - byte[] eotFrame = readProtocolMessageFrame(); - DataInputStream eotIn = new DataInputStream(new ByteArrayInputStream(eotFrame)); - byte ver = eotIn.readByte(); - byte eotType = eotIn.readByte(); - if (ver != version || eotType != END_OF_TRANSMISSION) { - throw new IOException("Invalid end-of-transmission marker. Expected version " + version + - " and type " + END_OF_TRANSMISSION + ", got version " + ver + " and type " + eotType); - } + if (fos != null) fos.close(); + + // Read EOT frame + byte[] eotFrame = readProtocolMessageFrame(); + try (DataInputStream eotIn = new DataInputStream(new ByteArrayInputStream(eotFrame))) { + byte ver = eotIn.readByte(); + byte eotType = eotIn.readByte(); + + if (ver != version || eotType != END_OF_TRANSMISSION) { + throw new IOException("Invalid end-of-transmission marker. Expected version " + version + + " and type " + END_OF_TRANSMISSION + ", got version " + ver + " and type " + eotType); + } + } - if (fos != null) { - fos.close(); - return destination; - } else { - return rawData; + return (rawData != null) ? rawData : destination; } } diff --git a/core/src/main/java/pl/skidam/protocol/NetUtils.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java similarity index 99% rename from core/src/main/java/pl/skidam/protocol/NetUtils.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java index 9e97badb..7d109e62 100644 --- a/core/src/main/java/pl/skidam/protocol/NetUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol; +package pl.skidam.automodpack_core.protocol; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; diff --git a/core/src/main/java/pl/skidam/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java similarity index 97% rename from core/src/main/java/pl/skidam/protocol/netty/NettyServer.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 59348d4f..31ffd6e2 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol.netty; +package pl.skidam.automodpack_core.protocol.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; @@ -12,8 +12,8 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.protocol.NetUtils; -import pl.skidam.protocol.netty.handler.ProtocolServerHandler; +import pl.skidam.automodpack_core.protocol.NetUtils; +import pl.skidam.automodpack_core.protocol.netty.handler.ProtocolServerHandler; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; import pl.skidam.automodpack_core.utils.Ip; import pl.skidam.automodpack_core.utils.ObservableMap; diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageDecoder.java similarity index 82% rename from core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageDecoder.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageDecoder.java index c8fdbe63..1a3a7274 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageDecoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageDecoder.java @@ -1,17 +1,17 @@ -package pl.skidam.protocol.netty.handler; +package pl.skidam.automodpack_core.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; -import pl.skidam.protocol.NetUtils; -import pl.skidam.protocol.netty.message.EchoMessage; -import pl.skidam.protocol.netty.message.FileRequestMessage; -import pl.skidam.protocol.netty.message.FileResponseMessage; -import pl.skidam.protocol.netty.message.RefreshRequestMessage; +import pl.skidam.automodpack_core.protocol.NetUtils; +import pl.skidam.automodpack_core.protocol.netty.message.EchoMessage; +import pl.skidam.automodpack_core.protocol.netty.message.FileRequestMessage; +import pl.skidam.automodpack_core.protocol.netty.message.FileResponseMessage; +import pl.skidam.automodpack_core.protocol.netty.message.RefreshRequestMessage; import java.util.List; -import static pl.skidam.protocol.NetUtils.*; +import static pl.skidam.automodpack_core.protocol.NetUtils.*; public class ProtocolMessageDecoder extends ByteToMessageDecoder { @Override diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java similarity index 91% rename from core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageEncoder.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java index b05cf0b4..2bb5ea08 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolMessageEncoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java @@ -1,11 +1,11 @@ -package pl.skidam.protocol.netty.handler; +package pl.skidam.automodpack_core.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import pl.skidam.protocol.netty.message.*; +import pl.skidam.automodpack_core.protocol.netty.message.*; -import static pl.skidam.protocol.NetUtils.*; +import static pl.skidam.automodpack_core.protocol.NetUtils.*; public class ProtocolMessageEncoder extends MessageToByteEncoder { @Override diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java similarity index 94% rename from core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolServerHandler.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java index e915362f..d563f9ea 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol.netty.handler; +package pl.skidam.automodpack_core.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -8,7 +8,7 @@ import java.util.List; -import static pl.skidam.protocol.NetUtils.*; +import static pl.skidam.automodpack_core.protocol.NetUtils.*; public class ProtocolServerHandler extends ByteToMessageDecoder { diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java similarity index 84% rename from core/src/main/java/pl/skidam/protocol/netty/handler/ServerMessageHandler.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java index 506c857a..539c2571 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ServerMessageHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol.netty.handler; +package pl.skidam.automodpack_core.protocol.netty.handler; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -10,12 +10,11 @@ import pl.skidam.automodpack_core.GlobalVariables; import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.modpack.ModpackContent; -import pl.skidam.protocol.netty.message.EchoMessage; -import pl.skidam.protocol.netty.message.FileRequestMessage; -import pl.skidam.protocol.netty.message.ProtocolMessage; -import pl.skidam.protocol.netty.message.RefreshRequestMessage; +import pl.skidam.automodpack_core.protocol.netty.message.EchoMessage; +import pl.skidam.automodpack_core.protocol.netty.message.FileRequestMessage; +import pl.skidam.automodpack_core.protocol.netty.message.ProtocolMessage; +import pl.skidam.automodpack_core.protocol.netty.message.RefreshRequestMessage; -import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.SocketAddress; @@ -25,7 +24,7 @@ import java.util.concurrent.CompletableFuture; import static pl.skidam.automodpack_core.GlobalVariables.*; -import static pl.skidam.protocol.NetUtils.*; +import static pl.skidam.automodpack_core.protocol.NetUtils.*; public class ServerMessageHandler extends SimpleChannelInboundHandler { @@ -72,7 +71,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); } - private void refreshModpackFiles(ChannelHandlerContext context, byte[][] FileHashesList) { + private void refreshModpackFiles(ChannelHandlerContext context, byte[][] FileHashesList) throws IOException { List hashes = new ArrayList<>(); for (byte[] hash : FileHashesList) { hashes.add(new String(hash)); @@ -118,7 +117,7 @@ private boolean validateSecret(SocketAddress address, byte[] secret) { return Secrets.isSecretValid(decodedSecret, address); } - private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) { + private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) throws IOException { final String sha1 = new String(bsha1, CharsetUtil.UTF_8); final Optional optionalPath = resolvePath(sha1); @@ -127,26 +126,26 @@ private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) { return; } - final File file = optionalPath.get().toFile(); + final Path path = optionalPath.get(); + final long fileSize = Files.size(path); // Send file response header: version, FILE_RESPONSE type, then file size (8 bytes) ByteBuf responseHeader = Unpooled.buffer(1 + 1 + 8); responseHeader.writeByte(clientProtocolVersion); responseHeader.writeByte(FILE_RESPONSE_TYPE); - responseHeader.writeLong(file.length()); + responseHeader.writeLong(fileSize); ctx.writeAndFlush(responseHeader); + if (fileSize == 0) { + sendEOT(ctx); + return; + } + // Stream the file using ChunkedFile (chunk size set to 131072 bytes = 128 KB) - suitable value for zstd try { - RandomAccessFile raf = new RandomAccessFile(file, "r"); + RandomAccessFile raf = new RandomAccessFile(path.toFile(), "r"); ChunkedFile chunkedFile = new ChunkedFile(raf, 0, raf.length(), 131072); - ctx.writeAndFlush(chunkedFile).addListener((ChannelFutureListener) future -> { - // After the file is sent, send an End-of-Transmission message. - ByteBuf eot = Unpooled.buffer(2); - eot.writeByte((byte) 1); - eot.writeByte(END_OF_TRANSMISSION); - ctx.writeAndFlush(eot); - }); + ctx.writeAndFlush(chunkedFile).addListener((ChannelFutureListener) future -> sendEOT(ctx)); } catch (IOException e) { sendError(ctx, (byte) 1, "File transfer error: " + e.getMessage()); } @@ -170,4 +169,11 @@ private void sendError(ChannelHandlerContext ctx, byte version, String errorMess ctx.writeAndFlush(errorBuf); ctx.channel().close(); } + + private void sendEOT(ChannelHandlerContext ctx) { + ByteBuf eot = Unpooled.buffer(2); + eot.writeByte((byte) 1); + eot.writeByte(END_OF_TRANSMISSION); + ctx.writeAndFlush(eot); + } } diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java similarity index 95% rename from core/src/main/java/pl/skidam/protocol/netty/handler/ZstdDecoder.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java index 72d7806e..a5f33778 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdDecoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol.netty.handler; +package pl.skidam.automodpack_core.protocol.netty.handler; import com.github.luben.zstd.Zstd; import io.netty.buffer.ByteBuf; diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java similarity index 93% rename from core/src/main/java/pl/skidam/protocol/netty/handler/ZstdEncoder.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java index f8181a7a..4a3f5c47 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ZstdEncoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol.netty.handler; +package pl.skidam.automodpack_core.protocol.netty.handler; import com.github.luben.zstd.Zstd; import io.netty.buffer.ByteBuf; diff --git a/core/src/main/java/pl/skidam/protocol/netty/message/EchoMessage.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/EchoMessage.java similarity index 76% rename from core/src/main/java/pl/skidam/protocol/netty/message/EchoMessage.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/EchoMessage.java index 52c410b6..e13a0110 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/message/EchoMessage.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/EchoMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.protocol.netty.message; +package pl.skidam.automodpack_core.protocol.netty.message; -import static pl.skidam.protocol.NetUtils.ECHO_TYPE; +import static pl.skidam.automodpack_core.protocol.NetUtils.ECHO_TYPE; public class EchoMessage extends ProtocolMessage { private final int dataLength; diff --git a/core/src/main/java/pl/skidam/protocol/netty/message/FileRequestMessage.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/FileRequestMessage.java similarity index 78% rename from core/src/main/java/pl/skidam/protocol/netty/message/FileRequestMessage.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/FileRequestMessage.java index 69cf3f82..82d1ef27 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/message/FileRequestMessage.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/FileRequestMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.protocol.netty.message; +package pl.skidam.automodpack_core.protocol.netty.message; -import static pl.skidam.protocol.NetUtils.FILE_REQUEST_TYPE; +import static pl.skidam.automodpack_core.protocol.NetUtils.FILE_REQUEST_TYPE; public class FileRequestMessage extends ProtocolMessage { private final int fileHashLength; diff --git a/core/src/main/java/pl/skidam/protocol/netty/message/FileResponseMessage.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/FileResponseMessage.java similarity index 76% rename from core/src/main/java/pl/skidam/protocol/netty/message/FileResponseMessage.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/FileResponseMessage.java index d33ddecf..eb568d34 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/message/FileResponseMessage.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/FileResponseMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.protocol.netty.message; +package pl.skidam.automodpack_core.protocol.netty.message; -import static pl.skidam.protocol.NetUtils.FILE_RESPONSE_TYPE; +import static pl.skidam.automodpack_core.protocol.NetUtils.FILE_RESPONSE_TYPE; public class FileResponseMessage extends ProtocolMessage { private final int dataLength; diff --git a/core/src/main/java/pl/skidam/protocol/netty/message/ProtocolMessage.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ProtocolMessage.java similarity index 91% rename from core/src/main/java/pl/skidam/protocol/netty/message/ProtocolMessage.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ProtocolMessage.java index 1385a6f4..69130883 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/message/ProtocolMessage.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ProtocolMessage.java @@ -1,4 +1,4 @@ -package pl.skidam.protocol.netty.message; +package pl.skidam.automodpack_core.protocol.netty.message; public abstract class ProtocolMessage { private final byte version; // 1 byte diff --git a/core/src/main/java/pl/skidam/protocol/netty/message/RefreshRequestMessage.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/RefreshRequestMessage.java similarity index 83% rename from core/src/main/java/pl/skidam/protocol/netty/message/RefreshRequestMessage.java rename to core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/RefreshRequestMessage.java index 7aec6287..173efa2d 100644 --- a/core/src/main/java/pl/skidam/protocol/netty/message/RefreshRequestMessage.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/RefreshRequestMessage.java @@ -1,6 +1,6 @@ -package pl.skidam.protocol.netty.message; +package pl.skidam.automodpack_core.protocol.netty.message; -import static pl.skidam.protocol.NetUtils.REFRESH_REQUEST_TYPE; +import static pl.skidam.automodpack_core.protocol.NetUtils.REFRESH_REQUEST_TYPE; public class RefreshRequestMessage extends ProtocolMessage { private final int fileHashesCount; diff --git a/core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java b/core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java deleted file mode 100644 index 32bf8422..00000000 --- a/core/src/main/java/pl/skidam/protocol/netty/client/DownloadClient.java +++ /dev/null @@ -1,183 +0,0 @@ -package pl.skidam.protocol.netty.client; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslProvider; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import io.netty.handler.stream.ChunkedWriteHandler; -import pl.skidam.automodpack_core.auth.Secrets; -import pl.skidam.protocol.netty.handler.*; -import pl.skidam.protocol.netty.message.FileRequestMessage; -import pl.skidam.protocol.netty.message.RefreshRequestMessage; - -import javax.net.ssl.SSLException; -import java.net.InetSocketAddress; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; - -import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; -import static pl.skidam.protocol.NetUtils.MAGIC_AMMC; - -public class DownloadClient extends NettyClient { - private final Map channels = new HashMap<>(); // channel, isBusy - private final EventLoopGroup group; - private final SslContext sslCtx; - private final Secrets.Secret secret; - private final DownloadClient downloadClient; - private final Semaphore channelLock = new Semaphore(0); - - public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, int poolSize) throws InterruptedException, SSLException { - this.downloadClient = this; - this.secret = secret; - - // Yes, we use the insecure because server uses self-signed cert and we have different way to verify the authenticity - // Via secret and fingerprint, so the encryption strength should be the same, correct me if I'm wrong, thanks - sslCtx = SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE) - .sslProvider(SslProvider.JDK) - .protocols("TLSv1.3") - .ciphers(Arrays.asList( - "TLS_AES_128_GCM_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_CHACHA20_POLY1305_SHA256")) - .build(); - - group = new NioEventLoopGroup(); - Bootstrap bootstrap = new Bootstrap(); - bootstrap.group(group) - .channel(NioSocketChannel.class) - .option(ChannelOption.SO_KEEPALIVE, true) - .handler(new ChannelInitializer<>() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline().addLast(MOD_ID, new ProtocolClientHandler(downloadClient)); - } - }); - - // Initialize channels and wait for the channels in pool. - for (int i = 0; i < poolSize; i++) { - Channel channel = bootstrap.connect(remoteAddress).sync().channel(); - ByteBuf msg = channel.alloc().buffer(4); - msg.writeInt(MAGIC_AMMC); - channel.writeAndFlush(msg); - } - - channelLock.acquire(poolSize); - } - - @Override - public void secureInit(ChannelHandlerContext ctx) { - ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); - ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); - ctx.pipeline().addLast("chunked-write", new ChunkedWriteHandler()); - ctx.pipeline().addLast("protocol-msg-decoder", new ProtocolMessageEncoder()); - } - - @Override - public void addChannel(Channel channel) { - channels.put(channel, new AtomicBoolean(false)); - } - - @Override - public void removeChannel(Channel channel) { - channels.remove(channel); - } - - @Override - public void releaseChannel() { - channelLock.release(); - } - - @Override - public Secrets.Secret getSecret() { - return secret; - } - - /** - * Downloads a file by its SHA-1 hash to the specified destination. - * Returns a CompletableFuture that completes when the download finishes. - */ - public CompletableFuture downloadFile(byte[] fileHash, Path destination) { - // Select first not busy channel - Channel channel = channels.entrySet().stream() - .filter(entry -> !entry.getValue().get()) - .findFirst() - .map(Map.Entry::getKey) - .orElseThrow(() -> new IllegalStateException("No available channels")); - - // Mark channel as busy - channels.get(channel).set(true); - - // Add a new FileDownloadHandler to process this download. - FileDownloadHandler downloadHandler = new FileDownloadHandler(destination); - channel.pipeline().addLast("download-handler", downloadHandler); - - byte[] bsecret = Base64.getUrlDecoder().decode(secret.secret()); - - // Build and send the file request (which carries the secret and file hash). - FileRequestMessage request = new FileRequestMessage((byte) 1, bsecret, fileHash); - channel.writeAndFlush(request); - - // Return the future that will complete when the download finishes. - return downloadHandler.getDownloadFuture().whenComplete((result, throwable) -> { - // Mark channel as not busy - channels.get(channel).set(false); - }); - } - - /** - * Downloads a file by its SHA-1 hash to the specified destination. - * Returns a CompletableFuture that completes when the download finishes. - */ - public CompletableFuture requestRefresh(byte[][] fileHashes) { - // Select first not busy channel - Channel channel = channels.entrySet().stream() - .filter(entry -> !entry.getValue().get()) - .findFirst() - .map(Map.Entry::getKey) - .orElseThrow(() -> new IllegalStateException("No available channels")); - - // Mark channel as busy - channels.get(channel).set(true); - - // Add a new FileDownloadHandler to process this download. - FileDownloadHandler downloadHandler = new FileDownloadHandler(null); - channel.pipeline().addLast("download-handler", downloadHandler); - - byte[] bsecret = Base64.getUrlDecoder().decode(secret.secret()); - - // Build and send the file request (which carries the secret and file hash). - RefreshRequestMessage request = new RefreshRequestMessage((byte) 1, bsecret, fileHashes); - channel.writeAndFlush(request); - - // Return the future that will complete when the download finishes. - return downloadHandler.getDownloadFuture().whenComplete((result, throwable) -> { - // Mark channel as not busy - channels.get(channel).set(false); - }); - } - - /** - * Closes all channels in the pool and shuts down the event loop. - */ - public void close() { - for (Channel channel : channels.keySet()) { - if (channel.isOpen()) { - channel.close(); - } - } - group.shutdownGracefully(); - } - - public SslContext getSslCtx() { - return sslCtx; - } -} diff --git a/core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java b/core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java deleted file mode 100644 index 151913f5..00000000 --- a/core/src/main/java/pl/skidam/protocol/netty/client/EchoClient.java +++ /dev/null @@ -1,125 +0,0 @@ -package pl.skidam.protocol.netty.client; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslProvider; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import pl.skidam.automodpack_core.auth.Secrets; -import pl.skidam.protocol.netty.handler.ProtocolClientHandler; -import pl.skidam.protocol.netty.handler.ProtocolMessageEncoder; -import pl.skidam.protocol.netty.message.EchoMessage; - -import javax.net.ssl.SSLException; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Semaphore; - -import static pl.skidam.protocol.NetUtils.MAGIC_AMMC; - -public class EchoClient extends NettyClient { - private final List channels = new ArrayList<>(); - private final EventLoopGroup group; - private final SslContext sslCtx; - private final EchoClient echoClient; - private final Semaphore channelLock = new Semaphore(0); - - public EchoClient(InetSocketAddress remoteAddress) throws InterruptedException, SSLException { - this.echoClient = this; - - // Yes, we use the insecure because server uses self-signed cert and we have different way to verify the authenticity - // Via secret and fingerprint, so the encryption strength should be the same, correct me if I'm wrong, thanks - sslCtx = SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE) - .sslProvider(SslProvider.JDK) - .protocols("TLSv1.3") - .ciphers(Arrays.asList( - "TLS_AES_128_GCM_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_CHACHA20_POLY1305_SHA256")) - .build(); - - group = new NioEventLoopGroup(); - Bootstrap bootstrap = new Bootstrap(); - bootstrap.group(group) - .channel(NioSocketChannel.class) - .option(ChannelOption.SO_KEEPALIVE, true) - .handler(new ChannelInitializer<>() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline().addLast(new ProtocolClientHandler(echoClient)); - } - }); - - // Initialize channels and wait for the channels in pool. - Channel channel = bootstrap.connect(remoteAddress).sync().channel(); - ByteBuf msg = channel.alloc().buffer(4); - msg.writeInt(MAGIC_AMMC); - channel.writeAndFlush(msg); - - channelLock.acquire(); - } - - @Override - public void secureInit(ChannelHandlerContext ctx) { - ctx.pipeline().addLast(new ProtocolMessageEncoder()); - } - - @Override - public void addChannel(Channel channel) { - channels.add(channel); - } - - @Override - public void removeChannel(Channel channel) { - channels.remove(channel); - } - - @Override - public void releaseChannel() { - channelLock.release(); - } - - @Override - public Secrets.Secret getSecret() { - return null; - } - - /** - * Downloads a file by its SHA-1 hash to the specified destination. - * Returns a CompletableFuture that completes when the download finishes. - */ - public CompletableFuture sendEcho(byte[] secret, byte[] data) { - Channel channel = channels.get(0); - - // Build and send the file request (which carries the secret and file hash). - EchoMessage request = new EchoMessage((byte) 1, secret, data); - channel.writeAndFlush(request); - - // Return the future that will complete when the download finishes. - return null; - } - - /** - * Closes all channels in the pool and shuts down the event loop. - */ - public void close() { - for (Channel channel : channels) { - if (channel.isOpen()) { - channel.close(); - } - } - group.shutdownGracefully(); - } - - public SslContext getSslCtx() { - return sslCtx; - } -} diff --git a/core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java b/core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java deleted file mode 100644 index 2b2cadd0..00000000 --- a/core/src/main/java/pl/skidam/protocol/netty/client/NettyClient.java +++ /dev/null @@ -1,15 +0,0 @@ -package pl.skidam.protocol.netty.client; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.ssl.SslContext; -import pl.skidam.automodpack_core.auth.Secrets; - -public abstract class NettyClient { - public abstract SslContext getSslCtx(); - public abstract void secureInit(ChannelHandlerContext ctx); - public abstract void addChannel(Channel channel); - public abstract void removeChannel(Channel channel); - public abstract void releaseChannel(); - public abstract Secrets.Secret getSecret(); -} diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java b/core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java deleted file mode 100644 index e12a6acb..00000000 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/FileDownloadHandler.java +++ /dev/null @@ -1,154 +0,0 @@ -package pl.skidam.protocol.netty.handler; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.util.ReferenceCountUtil; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Path; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; -import static pl.skidam.protocol.NetUtils.*; - -public class FileDownloadHandler extends ChannelInboundHandlerAdapter { - - private enum State { - WAITING_HEADER, - RECEIVING_FILE, - WAITING_EOT, - COMPLETED, - ERROR - } - - private State state = State.WAITING_HEADER; - private long expectedFileSize; - private long receivedBytes = 0; - private final Path destination; - private FileOutputStream fos; - private List rawFileData; - private final CompletableFuture downloadFuture = new CompletableFuture<>(); - private byte protocolVersion = 0; - - public FileDownloadHandler(Path destination) { - this.destination = destination; - } - - public CompletableFuture getDownloadFuture() { - return downloadFuture; - } - - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - if (destination != null) { - fos = new FileOutputStream(destination.toFile()); - } else { - rawFileData = new LinkedList<>(); - } - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - if (fos != null) { - fos.close(); - } - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (!(msg instanceof ByteBuf buf)) { - LOGGER.warn("Received non-ByteBuf message: {}", msg); - ctx.fireChannelRead(msg); - return; - } - - try { - // State machine to process the response - if (state == State.WAITING_HEADER) { - if (buf.readableBytes() < 10) { - return; - } - - protocolVersion = buf.readByte(); - byte type = buf.readByte(); - if (type == ERROR) { - int errLen = buf.readInt(); - byte[] errBytes = new byte[errLen]; - buf.readBytes(errBytes); - downloadFuture.completeExceptionally(new IOException("Server error: " + new String(errBytes))); - state = State.ERROR; - return; - } - if (type != FILE_RESPONSE_TYPE) { - downloadFuture.completeExceptionally(new IOException("Unexpected message type: " + type)); - state = State.ERROR; - return; - } - expectedFileSize = buf.readLong(); - state = State.RECEIVING_FILE; - } else if (state == State.RECEIVING_FILE) { - // In RECEIVING_FILE state, we write raw file data. - int readable = buf.readableBytes(); - long remaining = expectedFileSize - receivedBytes; - if (readable <= remaining) { - byte[] data = new byte[readable]; - buf.readBytes(data); - if (fos != null) { - fos.write(data); - } else { - rawFileData.add(data); - } - receivedBytes += readable; - } else { - // Read only the bytes that belong to the file. - byte[] data = new byte[(int) remaining]; - buf.readBytes(data); - if (fos != null) { - fos.write(data); - } else { - rawFileData.add(data); - } - receivedBytes += remaining; - state = State.WAITING_EOT; - } - if (receivedBytes == expectedFileSize) { - state = State.WAITING_EOT; - } - } else if (state == State.WAITING_EOT) { - if (buf.readableBytes() < 2) { - return; - } - - byte ver = buf.readByte(); - if (ver != protocolVersion) { - downloadFuture.completeExceptionally(new IOException("Expected protocol version: " + protocolVersion + ", got: " + ver)); - state = State.ERROR; - return; - } - byte type = buf.readByte(); - if (type != END_OF_TRANSMISSION) { - downloadFuture.completeExceptionally(new IOException("Expected EOT, got type: " + type)); - state = State.ERROR; - return; - } - state = State.COMPLETED; - Object result = destination != null ? destination : rawFileData; - downloadFuture.complete(result); - // Remove this handler now that download is complete. - ctx.pipeline().remove(this); - } - } finally { - ReferenceCountUtil.release(buf); - } - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - downloadFuture.completeExceptionally(cause); - ctx.close(); - } -} diff --git a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java b/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java deleted file mode 100644 index 40063f77..00000000 --- a/core/src/main/java/pl/skidam/protocol/netty/handler/ProtocolClientHandler.java +++ /dev/null @@ -1,96 +0,0 @@ -package pl.skidam.protocol.netty.handler; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.ssl.SslHandler; -import pl.skidam.protocol.NetUtils; -import pl.skidam.protocol.netty.client.NettyClient; - -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.List; - -import static pl.skidam.protocol.NetUtils.MAGIC_AMOK; - -public class ProtocolClientHandler extends ByteToMessageDecoder { - - private final NettyClient client; - - public ProtocolClientHandler(NettyClient client) { - this.client = client; - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - try { - if (in.readableBytes() < 4) { - return; - } - - int magic = in.getInt(0); - if (magic != MAGIC_AMOK) { - client.releaseChannel(); - } else { - // Consume the packet - in.skipBytes(in.readableBytes()); - - // Set up the pipeline for the protocol - SslHandler sslHandler = client.getSslCtx().newHandler(ctx.alloc()); - ctx.pipeline().addLast("tls", sslHandler); - - // Wait for SSL handshake to complete before sending data - sslHandler.handshakeFuture().addListener(future -> { - if (!future.isSuccess()) { - ctx.close(); - System.err.println("SSL handshake failed"); - return; - } - - try { - Certificate[] certs = sslHandler.engine().getSession().getPeerCertificates(); - if (certs == null || certs.length == 0 || certs.length > 3) { - return; - } - - for (Certificate cert : certs) { - if (cert instanceof X509Certificate x509Cert) { - String fingerprint = NetUtils.getFingerprint(x509Cert, client.getSecret().secret()); - if (fingerprint.equals(client.getSecret().fingerprint())) { - System.out.println("Server certificate verified, fingerprint: " + fingerprint); - client.secureInit(ctx); - break; - } - } - } - } catch (Exception e) { - e.printStackTrace(); - ctx.close(); - } finally { - if (ctx.channel().isOpen()) { - client.addChannel(ctx.channel()); - } - client.releaseChannel(); - } - }); - } - - ctx.pipeline().remove(this); // Always remove this handler after processing - } catch (Exception e) { - e.printStackTrace(); - ctx.close(); - } - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - client.removeChannel(ctx.channel()); - client.releaseChannel(); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } -} \ No newline at end of file diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 04fcac7b..ea1990b0 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -3,7 +3,7 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.protocol.DownloadClient; +import pl.skidam.automodpack_core.protocol.DownloadClient; import pl.skidam.automodpack_core.utils.*; import pl.skidam.automodpack_loader_core.ReLauncher; import pl.skidam.automodpack_loader_core.screen.ScreenManager; @@ -219,7 +219,7 @@ public void startUpdate() { newDownloadedFiles.clear(); int wholeQueue = serverModpackContent.list.size(); - LOGGER.info("In queue left {} files to download ({}kb)", wholeQueue, totalBytesToDownload / 1024); + LOGGER.info("In queue left {} files to download ({}MB)", wholeQueue, totalBytesToDownload / 1024 / 1024); downloadManager = new DownloadManager(totalBytesToDownload); new ScreenManager().download(downloadManager, getModpackName()); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 6758869f..59bb0254 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -6,14 +6,13 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.protocol.DownloadClient; +import pl.skidam.automodpack_core.protocol.DownloadClient; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.FileInspection; import pl.skidam.automodpack_core.utils.ModpackContentTools; import java.io.*; import java.net.*; -import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; @@ -462,66 +461,6 @@ public static Optional refreshServerModpackContent(I return Optional.empty(); } - - public static Optional refreshServerModpackContent(String link, Secrets.Secret secret, String body) { - // send custom http body request to get modpack content, rest the same as getServerModpackContent - if (link == null || body == null) { - throw new IllegalArgumentException("Link or body is null"); - } - - HttpURLConnection connection = null; - - try { - connection = (HttpURLConnection) new URL(link + "refresh").openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty(SECRET_REQUEST_HEADER, secret.secret()); - - return connectionToModpack(connection, body); - } catch (Exception e) { - LOGGER.error("Error while getting server modpack content", e); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - return Optional.empty(); - } - - public static Optional connectionToModpack(HttpURLConnection connection) { - return connectionToModpack(connection, null); - } - - public static Optional connectionToModpack(HttpURLConnection connection, String body) { - int responseCode = -1; - try { - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "github/skidamek/automodpack/" + AM_VERSION); - if (body != null) { - connection.setDoOutput(true); - connection.getOutputStream().write(body.getBytes(StandardCharsets.UTF_8)); - } - connection.connect(); - - responseCode = connection.getResponseCode(); - - if (responseCode == 200) { - return parseStreamToModpack(connection.getInputStream()); - } else { - LOGGER.error("Couldn't connect to modpack server: {} Response Code: {}", connection.getURL(), responseCode); - } - - } catch (SocketException | SocketTimeoutException e) { - LOGGER.error("Couldn't connect to modpack server: {} Response Code: {} Error: {}", connection.getURL(), responseCode, e.getCause()); - } catch (Exception e) { - LOGGER.error("Error while getting server modpack content", e); - } - - return Optional.empty(); - } - public static Optional parseStreamToModpack(List rawBytes) { String response = null; @@ -569,44 +508,6 @@ public static Optional parseStreamToModpack(List parseStreamToModpack(InputStream stream) { - - String response = null; - - try (InputStreamReader isr = new InputStreamReader(stream)) { - JsonElement element = JsonParser.parseReader(isr); // Needed to parse by deprecated method because of older minecraft versions (<1.17.1) - if (element != null && !element.isJsonArray()) { - JsonObject obj = element.getAsJsonObject(); - response = obj.toString(); - } - } catch (Exception e) { - LOGGER.error("Couldn't parse modpack content", e); - } - - if (response == null) { - LOGGER.error("Couldn't parse modpack content"); - return Optional.empty(); - } - - Jsons.ModpackContentFields serverModpackContent = GSON.fromJson(response, Jsons.ModpackContentFields.class); - - if (serverModpackContent == null) { - LOGGER.error("Couldn't parse modpack content"); - return Optional.empty(); - } - - if (serverModpackContent.list.isEmpty()) { - LOGGER.error("Modpack content is empty!"); - return Optional.empty(); - } - - if (potentiallyMalicious(serverModpackContent)) { - return Optional.empty(); - } - - return Optional.of(serverModpackContent); - } - // check if modpackContent is valid/isn't malicious public static boolean potentiallyMalicious(Jsons.ModpackContentFields serverModpackContent) { String modpackName = serverModpackContent.modpackName; diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 84b31f9a..062cb0da 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -3,7 +3,7 @@ import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; import pl.skidam.automodpack_core.utils.FileInspection; -import pl.skidam.protocol.DownloadClient; +import pl.skidam.automodpack_core.protocol.DownloadClient; import java.io.*; import java.net.*; @@ -70,9 +70,9 @@ private void downloadTask(FileInspection.HashPathPair hashPathPair, QueuedDownlo } catch (InterruptedException e) { interrupted = true; } catch (SocketTimeoutException e) { - LOGGER.warn("Timeout - {} - {} - {}", queuedDownload.file, e, e.getStackTrace()); + LOGGER.warn("Timeout - {} - {} - {}", queuedDownload.file, e, e.fillInStackTrace()); } catch (Exception e) { - LOGGER.warn("Error while downloading file - {} - {} - {}", queuedDownload.file, e, e.getStackTrace()); + LOGGER.warn("Error while downloading file - {} - {} - {}", queuedDownload.file, e, e.fillInStackTrace()); } finally { synchronized (downloadsInProgress) { downloadsInProgress.remove(hashPathPair); @@ -141,7 +141,7 @@ private synchronized void downloadNext() { } } - private void hostDownloadFile(FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException, ExecutionException { + private void hostDownloadFile(FileInspection.HashPathPair hashPathPair, QueuedDownload queuedDownload) throws IOException, InterruptedException { Path outFile = queuedDownload.file; if (Files.exists(outFile)) { @@ -152,15 +152,7 @@ private void hostDownloadFile(FileInspection.HashPathPair hashPathPair, QueuedDo } } - if (outFile.getParent() != null) { - Files.createDirectories(outFile.getParent()); - } - - if (!Files.exists(outFile)) { - // Windows? #302 - outFile.toFile().createNewFile(); -// Files.createFile(outFile); - } + CustomFileUtils.setupFilePaths(outFile); var future = downloadClient.downloadFile(hashPathPair.hash().getBytes(StandardCharsets.UTF_8), outFile, (bytes) -> { bytesDownloaded += bytes; @@ -181,15 +173,7 @@ private void httpDownloadFile(String url, FileInspection.HashPathPair hashPathPa } } - if (outFile.getParent() != null) { - Files.createDirectories(outFile.getParent()); - } - - if (!Files.exists(outFile)) { - // Windows? #302 - outFile.toFile().createNewFile(); -// Files.createFile(outFile); - } + CustomFileUtils.setupFilePaths(outFile); URLConnection connection = getHttpConnection(url); diff --git a/loader/loader-fabric-core.gradle.kts b/loader/loader-fabric-core.gradle.kts index 8343deea..25ef205c 100644 --- a/loader/loader-fabric-core.gradle.kts +++ b/loader/loader-fabric-core.gradle.kts @@ -25,7 +25,7 @@ dependencies { compileOnly("com.google.code.gson:gson:2.10.1") compileOnly("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.tomlj:tomlj:1.1.1") - implementation("org.bouncycastle:bcprov-jdk18on:1.80") +// implementation("org.bouncycastle:bcprov-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("com.github.luben:zstd-jni:1.5.7-1") @@ -42,7 +42,6 @@ configurations { // TODO: make it less messy tasks.named("shadowJar") { archiveClassifier.set("") - mergeServiceFiles() from(project(":core").sourceSets.main.get().output) from(project(":loader-core").sourceSets.main.get().output) @@ -53,9 +52,12 @@ tasks.named("shadowJar") { // Include the tomlj dependency in the shadow jar configurations = listOf(project.configurations.getByName("shadowImplementation")) - relocate("org.antlr.v4", "reloc.org.antlr.v4") - relocate("org.tomlj", "reloc.org.tomlj") - relocate("org.checkerframework", "reloc.org.checkerframework") + val reloc = "am_libs" + relocate("org.antlr", "${reloc}.org.antlr") + relocate("org.tomlj", "${reloc}.org.tomlj") + relocate("org.checkerframework", "${reloc}.org.checkerframework") + relocate("com.github.luben", "${reloc}.com.github.luben") + relocate("org.bouncycastle", "${reloc}.org.bouncycastle") relocate("pl.skidam.automodpack_loader_core_fabric", "pl.skidam.automodpack_loader_core") relocate("pl.skidam.automodpack_loader_master_core_fabric", "pl.skidam.automodpack_loader_core") @@ -70,6 +72,8 @@ tasks.named("shadowJar") { manifest { attributes["AutoModpack-Version"] = version } + + mergeServiceFiles() } java { diff --git a/loader/loader-forge.gradle.kts b/loader/loader-forge.gradle.kts index 58615de2..7fa43208 100644 --- a/loader/loader-forge.gradle.kts +++ b/loader/loader-forge.gradle.kts @@ -37,7 +37,7 @@ dependencies { compileOnly("com.google.code.gson:gson:2.10.1") compileOnly("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.tomlj:tomlj:1.1.1") - implementation("org.bouncycastle:bcprov-jdk18on:1.80") +// implementation("org.bouncycastle:bcprov-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("com.github.luben:zstd-jni:1.5.7-1") @@ -65,9 +65,12 @@ tasks.named("shadowJar") { // Include the tomlj dependency in the shadow jar configurations = listOf(project.configurations.getByName("shadowImplementation")) - relocate("org.antlr.v4", "reloc.org.antlr.v4") - relocate("org.tomlj", "reloc.org.tomlj") - relocate("org.checkerframework", "reloc.org.checkerframework") + val reloc = "am_libs" + relocate("org.antlr", "${reloc}.org.antlr") + relocate("org.tomlj", "${reloc}.org.tomlj") + relocate("org.checkerframework", "${reloc}.org.checkerframework") + relocate("com.github.luben", "${reloc}.com.github.luben") + relocate("org.bouncycastle", "${reloc}.org.bouncycastle") if (project.name.contains("neoforge")) { relocate("pl.skidam.automodpack_loader_core_neoforge", "pl.skidam.automodpack_loader_core") diff --git a/src/main/java/pl/skidam/automodpack/init/Common.java b/src/main/java/pl/skidam/automodpack/init/Common.java index d8147ce1..a973c2af 100644 --- a/src/main/java/pl/skidam/automodpack/init/Common.java +++ b/src/main/java/pl/skidam/automodpack/init/Common.java @@ -6,7 +6,7 @@ import pl.skidam.automodpack.networking.ModPackets; import pl.skidam.automodpack_core.modpack.Modpack; import pl.skidam.automodpack_core.loader.LoaderManagerService; -import pl.skidam.protocol.netty.NettyServer; +import pl.skidam.automodpack_core.protocol.netty.NettyServer; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/pl/skidam/automodpack/loader/GameCall.java b/src/main/java/pl/skidam/automodpack/loader/GameCall.java index 2bfc27d5..9bb5229e 100644 --- a/src/main/java/pl/skidam/automodpack/loader/GameCall.java +++ b/src/main/java/pl/skidam/automodpack/loader/GameCall.java @@ -14,7 +14,7 @@ public class GameCall implements GameCallService { @Override - public boolean canPlayerJoin(SocketAddress address, String id) { + public boolean isPlayerAuthorized(SocketAddress address, String id) { UUID uuid = UUID.fromString(id); String playerName = "Player"; // mock name, name matters less than UUID anyway GameProfile profile = new GameProfile(uuid, playerName); diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java index a3103374..9e7a8670 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import pl.skidam.automodpack_core.GlobalVariables; -import pl.skidam.protocol.netty.handler.ProtocolServerHandler; +import pl.skidam.automodpack_core.protocol.netty.handler.ProtocolServerHandler; import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; import static pl.skidam.automodpack_core.GlobalVariables.hostServer; From fb4afbdbba06ccee115d4e992ff3cba6e0f1b0ab Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 10:52:50 +0100 Subject: [PATCH 17/50] dont relocate zstd --- loader/loader-fabric-core.gradle.kts | 2 +- loader/loader-forge.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/loader/loader-fabric-core.gradle.kts b/loader/loader-fabric-core.gradle.kts index 25ef205c..b10e3388 100644 --- a/loader/loader-fabric-core.gradle.kts +++ b/loader/loader-fabric-core.gradle.kts @@ -56,7 +56,7 @@ tasks.named("shadowJar") { relocate("org.antlr", "${reloc}.org.antlr") relocate("org.tomlj", "${reloc}.org.tomlj") relocate("org.checkerframework", "${reloc}.org.checkerframework") - relocate("com.github.luben", "${reloc}.com.github.luben") +// relocate("com.github.luben", "${reloc}.com.github.luben") // cant relocate - natives relocate("org.bouncycastle", "${reloc}.org.bouncycastle") relocate("pl.skidam.automodpack_loader_core_fabric", "pl.skidam.automodpack_loader_core") diff --git a/loader/loader-forge.gradle.kts b/loader/loader-forge.gradle.kts index 7fa43208..58ce2958 100644 --- a/loader/loader-forge.gradle.kts +++ b/loader/loader-forge.gradle.kts @@ -69,7 +69,7 @@ tasks.named("shadowJar") { relocate("org.antlr", "${reloc}.org.antlr") relocate("org.tomlj", "${reloc}.org.tomlj") relocate("org.checkerframework", "${reloc}.org.checkerframework") - relocate("com.github.luben", "${reloc}.com.github.luben") +// relocate("com.github.luben", "${reloc}.com.github.luben") // cant relocate - natives relocate("org.bouncycastle", "${reloc}.org.bouncycastle") if (project.name.contains("neoforge")) { From d243b30e5fec9ef2e22da4da21739a00265d0651 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 12:15:34 +0100 Subject: [PATCH 18/50] Added `automodpack host connections` command and don't use compression on local connections --- .../protocol/DownloadClient.java | 30 ++++++++++++----- .../protocol/netty/NettyServer.java | 24 +++++++++++--- .../netty/handler/ProtocolServerHandler.java | 10 ++++-- .../netty/handler/ServerMessageHandler.java | 29 +++++++++++++--- .../protocol/netty/handler/ZstdDecoder.java | 14 +------- .../protocol/netty/handler/ZstdEncoder.java | 3 -- .../utils/{Ip.java => AddressHelpers.java} | 2 +- .../utils/DownloadManager.java | 3 ++ loader/loader-forge.gradle.kts | 3 +- .../skidam/automodpack/loader/GameCall.java | 11 +------ .../skidam/automodpack/modpack/Commands.java | 33 ++++++++++++++++++- .../automodpack/modpack/GameHelpers.java | 16 +++++++++ .../networking/packet/HandshakeS2CPacket.java | 4 +-- 13 files changed, 131 insertions(+), 51 deletions(-) rename core/src/main/java/pl/skidam/automodpack_core/utils/{Ip.java => AddressHelpers.java} (99%) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index 51510a1d..6fbcf316 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -3,6 +3,7 @@ import pl.skidam.automodpack_core.auth.Secrets; import com.github.luben.zstd.Zstd; import pl.skidam.automodpack_core.callbacks.IntCallback; +import pl.skidam.automodpack_core.utils.AddressHelpers; import javax.net.ssl.*; import java.io.*; @@ -67,6 +68,8 @@ public void close() { for (Connection conn : connections) { conn.close(); } + + connections.clear(); } } @@ -78,6 +81,7 @@ public void close() { class Connection { private static final byte PROTOCOL_VERSION = 1; + private final boolean isLocalConnection; // Don't compress local connections private final byte[] secretBytes; private final SSLSocket socket; private final DataInputStream in; @@ -138,6 +142,9 @@ public Connection(InetSocketAddress remoteAddress, Secrets.Secret secret) throws throw new IOException("Server certificate validation failed"); } + String address = remoteAddress.getAddress().toString(); + isLocalConnection = AddressHelpers.isLocal(address); + secretBytes = Base64.getUrlDecoder().decode(secret.secret()); // Now use the SSL socket for further communication. @@ -243,10 +250,13 @@ private void finalBlock(Exception exception) { * Message framing: [int: compressedLength][int: originalLength][compressed payload]. */ private void writeProtocolMessage(byte[] payload) throws IOException { - byte[] compressed = Zstd.compress(payload); - out.writeInt(compressed.length); - out.writeInt(payload.length); - out.write(compressed); + if (isLocalConnection) { + out.write(payload); + } else { + byte[] compressed = Zstd.compress(payload); + out.writeInt(payload.length); + out.write(compressed); + } out.flush(); } @@ -254,11 +264,13 @@ private void writeProtocolMessage(byte[] payload) throws IOException { * Reads one framed protocol message, decompressing it. */ private byte[] readProtocolMessageFrame() throws IOException { - int compLength = in.readInt(); - int origLength = in.readInt(); - byte[] compData = new byte[compLength]; - in.readFully(compData); - return Zstd.decompress(compData, origLength); + if (isLocalConnection) { + return in.readAllBytes(); + } else { + int origLength = in.readInt(); + byte[] compData = in.readAllBytes(); + return Zstd.decompress(compData, origLength); + } } /** diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 31ffd6e2..1a14d8e8 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -15,7 +15,7 @@ import pl.skidam.automodpack_core.protocol.NetUtils; import pl.skidam.automodpack_core.protocol.netty.handler.ProtocolServerHandler; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; -import pl.skidam.automodpack_core.utils.Ip; +import pl.skidam.automodpack_core.utils.AddressHelpers; import pl.skidam.automodpack_core.utils.ObservableMap; import java.net.InetAddress; @@ -31,13 +31,29 @@ // TODO: clean up this class public class NettyServer { - + private final Map connections = Collections.synchronizedMap(new HashMap<>()); private final Map paths = Collections.synchronizedMap(new HashMap<>()); private ChannelFuture serverChannel; private Boolean shouldHost = false; // needed for stop modpack hosting for minecraft port private X509Certificate certificate; private SslContext sslCtx; + public void addConnection(Channel channel, String secret) { + synchronized (connections) { + connections.put(channel, secret); + } + } + + public void removeConnection(Channel channel) { + synchronized (connections) { + connections.remove(channel); + } + } + + public Map getConnections() { + return connections; + } + public void addPaths(ObservableMap paths) { this.paths.putAll(paths.getMap()); paths.addOnPutCallback(this.paths::put); @@ -191,7 +207,7 @@ private boolean canStart() { } if (serverConfig.updateIpsOnEveryStart || (serverConfig.hostIp == null || serverConfig.hostIp.isEmpty())) { - String publicIp = Ip.getPublicIp(); + String publicIp = AddressHelpers.getPublicIp(); if (publicIp != null) { serverConfig.hostIp = publicIp; ConfigTools.save(serverConfigFile, serverConfig); @@ -204,7 +220,7 @@ private boolean canStart() { if (serverConfig.updateIpsOnEveryStart || (serverConfig.hostLocalIp == null || serverConfig.hostLocalIp.isEmpty())) { try { - serverConfig.hostLocalIp = Ip.getLocalIp(); + serverConfig.hostLocalIp = AddressHelpers.getLocalIp(); ConfigTools.save(serverConfigFile, serverConfig); LOGGER.warn("Setting Host local IP to {}", serverConfig.hostLocalIp); } catch (Exception e) { diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java index d563f9ea..535a32c5 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java @@ -5,6 +5,7 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.ssl.SslContext; import io.netty.handler.stream.ChunkedWriteHandler; +import pl.skidam.automodpack_core.utils.AddressHelpers; import java.util.List; @@ -39,10 +40,15 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t var handlers = ctx.pipeline().toMap(); handlers.forEach((name, handler) -> ctx.pipeline().remove(handler)); + String address = ctx.channel().remoteAddress().toString(); + boolean isLocalConnection = AddressHelpers.isLocal(address); + // Set up the pipeline for our protocol ctx.pipeline().addLast("tls", sslCtx.newHandler(ctx.alloc())); - ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); - ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); + if (!isLocalConnection) { + ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); + ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); + } ctx.pipeline().addLast("chunked-write", new ChunkedWriteHandler()); ctx.pipeline().addLast("protocol-msg-decoder", new ProtocolMessageDecoder()); ctx.pipeline().addLast("msg-handler", new ServerMessageHandler()); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java index 539c2571..5d4bdb65 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java @@ -28,7 +28,13 @@ public class ServerMessageHandler extends SimpleChannelInboundHandler { - private static byte clientProtocolVersion = 0; + private final Map secretLookup = new HashMap<>(); + private byte clientProtocolVersion = 0; + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + hostServer.removeConnection(ctx.channel()); + } @Override protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) throws Exception { @@ -36,7 +42,7 @@ protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) thro SocketAddress address = ctx.channel().remoteAddress(); // Validate the secret - if (!validateSecret(address, msg.getSecret())) { + if (!validateSecret(ctx, address, msg.getSecret())) { sendError(ctx, clientProtocolVersion, "Authentication failed"); return; } @@ -112,9 +118,22 @@ private void refreshModpackFiles(ChannelHandlerContext context, byte[][] FileHas } - private boolean validateSecret(SocketAddress address, byte[] secret) { - String decodedSecret = Base64.getUrlEncoder().withoutPadding().encodeToString(secret); - return Secrets.isSecretValid(decodedSecret, address); + private boolean validateSecret(ChannelHandlerContext ctx, SocketAddress address, byte[] secret) { + String decodedSecret = secretLookup.get(secret); + boolean addConnection = false; + if (decodedSecret == null) { + decodedSecret = Base64.getUrlEncoder().withoutPadding().encodeToString(secret); + addConnection = true; + secretLookup.put(secret, decodedSecret); + } + + boolean valid = Secrets.isSecretValid(decodedSecret, address); + + if (addConnection && valid) { + hostServer.addConnection(ctx.channel(), decodedSecret); + } + + return valid; } private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) throws IOException { diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java index a5f33778..df0adb83 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java @@ -11,24 +11,12 @@ public class ZstdDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - if (in.readableBytes() < 8) { - return; - } - - int length = in.readInt(); int originalLength = in.readInt(); - if (in.readableBytes() < length) { - in.resetReaderIndex(); - return; - } - - byte[] compressed = new byte[length]; + byte[] compressed = new byte[in.readableBytes()]; in.readBytes(compressed); -// var time = System.currentTimeMillis(); byte[] decompressed = Zstd.decompress(compressed, originalLength); -// LOGGER.info("Decompression time: {}ms. Saved {} bytes", System.currentTimeMillis() - time, originalLength - length); ByteBuf decompressedBuf = ctx.alloc().buffer(decompressed.length); decompressedBuf.writeBytes(decompressed); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java index 4a3f5c47..57b69956 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java @@ -12,11 +12,8 @@ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throw byte[] input = new byte[msg.readableBytes()]; msg.readBytes(input); -// var time = System.currentTimeMillis(); byte[] compressed = Zstd.compress(input); -// LOGGER.info("Compression time: {}ms. Saved {} bytes", System.currentTimeMillis() - time, input.length - compressed.length); - out.writeInt(compressed.length); out.writeInt(input.length); out.writeBytes(compressed); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java similarity index 99% rename from core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java rename to core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java index 79bea785..bb93e573 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/Ip.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java @@ -6,7 +6,7 @@ import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; -public class Ip { +public class AddressHelpers { public static String getPublicIp() { String[] services = { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 062cb0da..2e82a8a2 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -255,6 +255,9 @@ public void cancelAllAndShutdown() { downloaded = 0; addedToQueue = 0; + if (downloadClient != null) { + downloadClient.close(); + } DOWNLOAD_EXECUTOR.shutdownNow(); try { diff --git a/loader/loader-forge.gradle.kts b/loader/loader-forge.gradle.kts index 58ce2958..40ecd340 100644 --- a/loader/loader-forge.gradle.kts +++ b/loader/loader-forge.gradle.kts @@ -57,7 +57,6 @@ configurations { tasks.named("shadowJar") { archiveClassifier.set("") - mergeServiceFiles() from(project(":core").sourceSets.main.get().output) from(project(":loader-core").sourceSets.main.get().output) @@ -85,6 +84,8 @@ tasks.named("shadowJar") { manifest { attributes["AutoModpack-Version"] = version } + + mergeServiceFiles() } java { diff --git a/src/main/java/pl/skidam/automodpack/loader/GameCall.java b/src/main/java/pl/skidam/automodpack/loader/GameCall.java index 9bb5229e..f6ead6f8 100644 --- a/src/main/java/pl/skidam/automodpack/loader/GameCall.java +++ b/src/main/java/pl/skidam/automodpack/loader/GameCall.java @@ -1,12 +1,10 @@ package pl.skidam.automodpack.loader; import com.mojang.authlib.GameProfile; -import net.minecraft.util.UserCache; import pl.skidam.automodpack.modpack.GameHelpers; import pl.skidam.automodpack_core.loader.GameCallService; import java.net.SocketAddress; -import java.util.UUID; import static pl.skidam.automodpack.init.Common.server; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -15,14 +13,7 @@ public class GameCall implements GameCallService { @Override public boolean isPlayerAuthorized(SocketAddress address, String id) { - UUID uuid = UUID.fromString(id); - String playerName = "Player"; // mock name, name matters less than UUID anyway - GameProfile profile = new GameProfile(uuid, playerName); - - UserCache userCache = server.getUserCache(); - if (userCache != null) { - profile = userCache.getByUuid(uuid).orElse(profile); - } + GameProfile profile = GameHelpers.getPlayerProfile(id); if (server == null) { LOGGER.error("Server is null?"); diff --git a/src/main/java/pl/skidam/automodpack/modpack/Commands.java b/src/main/java/pl/skidam/automodpack/modpack/Commands.java index 40ad1e35..db41b24a 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/Commands.java +++ b/src/main/java/pl/skidam/automodpack/modpack/Commands.java @@ -1,5 +1,6 @@ package pl.skidam.automodpack.modpack; +import com.mojang.authlib.GameProfile; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.context.CommandContext; @@ -8,9 +9,12 @@ import net.minecraft.util.Util; import pl.skidam.automodpack.client.ui.versioned.VersionedCommandSource; import pl.skidam.automodpack.client.ui.versioned.VersionedText; +import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; +import java.util.Set; + import static net.minecraft.server.command.CommandManager.literal; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -39,6 +43,10 @@ public static void register(CommandDispatcher dispatcher) { .requires((source) -> source.hasPermissionLevel(3)) .executes(Commands::restartModpackHost) ) + .then(literal("connections") + .requires((source) -> source.hasPermissionLevel(3)) + .executes(Commands::connections) + ) ) .then(literal("config") .requires((source) -> source.hasPermissionLevel(3)) @@ -50,6 +58,29 @@ public static void register(CommandDispatcher dispatcher) { ); } + private static int connections(CommandContext serverCommandSourceCommandContext) { + Util.getMainWorkerExecutor().execute(() -> { + var connections = hostServer.getConnections(); + var secrets = Set.copyOf(connections.values()); + + send(serverCommandSourceCommandContext, String.format("Active connections: %d Unique connections: %d ", connections.size(), secrets.size()), Formatting.YELLOW, false); + + for (String secret : secrets) { + var playerSecretPair = SecretsStore.getHostSecret(secret); + if (playerSecretPair == null) continue; + + String playerId = playerSecretPair.getKey(); + GameProfile profile = GameHelpers.getPlayerProfile(playerId); + + long connNum = connections.values().stream().filter(secret::equals).count(); + + send(serverCommandSourceCommandContext, String.format("Player: %s (%s) is downloading modpack using %d connections", profile.getName(), playerId, connNum), Formatting.GREEN, false); + } + }); + + return Command.SINGLE_SUCCESS; + } + private static int reload(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { var tempServerConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFields.class); @@ -134,7 +165,7 @@ private static int modpackHostAbout(CommandContext context) private static int about(CommandContext context) { send(context, "AutoModpack", Formatting.GREEN, AM_VERSION, Formatting.WHITE, false); send(context, "/automodpack generate", Formatting.YELLOW, false); - send(context, "/automodpack host start/stop/restart", Formatting.YELLOW, false); + send(context, "/automodpack host start/stop/restart/connections", Formatting.YELLOW, false); send(context, "/automodpack config reload", Formatting.YELLOW, false); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java b/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java index f0a29a78..1fc9bcd9 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java +++ b/src/main/java/pl/skidam/automodpack/modpack/GameHelpers.java @@ -1,8 +1,10 @@ package pl.skidam.automodpack.modpack; import com.mojang.authlib.GameProfile; +import net.minecraft.util.UserCache; import java.net.SocketAddress; +import java.util.UUID; import static pl.skidam.automodpack.init.Common.server; @@ -23,4 +25,18 @@ public static boolean isPlayerAuthorized(SocketAddress address, GameProfile prof return true; } + + // Method to get GameProfile from UUID with accounting for a fact that this player may not be on the server right now + public static GameProfile getPlayerProfile(String id) { + UUID uuid = UUID.fromString(id); + String playerName = "Player"; // mock name, name matters less than UUID anyway + GameProfile profile = new GameProfile(uuid, playerName); + + UserCache userCache = server.getUserCache(); + if (userCache != null) { + profile = userCache.getByUuid(uuid).orElse(profile); + } + + return profile; + } } diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index de3055d4..065fe5f6 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -18,7 +18,7 @@ import pl.skidam.automodpack.networking.server.ServerLoginNetworking; import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.auth.SecretsStore; -import pl.skidam.automodpack_core.utils.Ip; +import pl.skidam.automodpack_core.utils.AddressHelpers; import static pl.skidam.automodpack.networking.ModPackets.DATA; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -96,7 +96,7 @@ public static void handleHandshake(ClientConnection connection, GameProfile prof String addressToSend; // If the player is connecting locally, use the local host IP - if (Ip.isLocal(playerIp)) { + if (AddressHelpers.isLocal(playerIp)) { addressToSend = serverConfig.hostLocalIp; } else { addressToSend = serverConfig.hostIp; From 62ae8b90de192328158d1bf3c1b692d43d857630 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 16:12:05 +0100 Subject: [PATCH 19/50] Fixed to host and client --- .../protocol/DownloadClient.java | 42 ++++++++++++++----- .../automodpack_core/protocol/NetUtils.java | 3 ++ .../netty/handler/ProtocolServerHandler.java | 13 +++--- .../protocol/netty/handler/ZstdDecoder.java | 37 +++++++++++++++- .../protocol/netty/handler/ZstdEncoder.java | 8 ++++ .../utils/AddressHelpers.java | 10 ++++- .../client/audio/AudioManager.java | 12 +++--- .../skidam/automodpack/init/FabricInit.java | 8 ++-- .../skidam/automodpack/init/NeoForgeInit.java | 14 +++---- .../skidam/automodpack/modpack/Commands.java | 6 +-- .../networking/packet/HandshakeS2CPacket.java | 6 ++- stonecutter.gradle.kts | 2 +- 12 files changed, 119 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index 6fbcf316..adc3dcd5 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -1,5 +1,6 @@ package pl.skidam.automodpack_core.protocol; +import pl.skidam.automodpack_core.GlobalVariables; import pl.skidam.automodpack_core.auth.Secrets; import com.github.luben.zstd.Zstd; import pl.skidam.automodpack_core.callbacks.IntCallback; @@ -35,8 +36,14 @@ public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, in } private synchronized Connection getFreeConnection() { - for (Connection conn : connections) { + Iterator iterator = connections.iterator(); + while (iterator.hasNext()) { + Connection conn = iterator.next(); if (!conn.isBusy()) { + if (!conn.isActive()) { + iterator.remove(); + return getFreeConnection(); + } conn.setBusy(true); return conn; } @@ -81,7 +88,7 @@ public void close() { class Connection { private static final byte PROTOCOL_VERSION = 1; - private final boolean isLocalConnection; // Don't compress local connections + private final boolean useCompression; // Don't compress local connections private final byte[] secretBytes; private final SSLSocket socket; private final DataInputStream in; @@ -89,6 +96,10 @@ class Connection { private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final AtomicBoolean busy = new AtomicBoolean(false); + public boolean isActive() { + return !socket.isClosed(); + } + /** * Creates a new connection by first opening a plain TCP socket, * sending the AMMC magic, waiting for the AMOK reply, and then upgrading to TLS. @@ -142,9 +153,7 @@ public Connection(InetSocketAddress remoteAddress, Secrets.Secret secret) throws throw new IOException("Server certificate validation failed"); } - String address = remoteAddress.getAddress().toString(); - isLocalConnection = AddressHelpers.isLocal(address); - + useCompression = !AddressHelpers.isLocal(remoteAddress); secretBytes = Base64.getUrlDecoder().decode(secret.secret()); // Now use the SSL socket for further communication. @@ -250,10 +259,12 @@ private void finalBlock(Exception exception) { * Message framing: [int: compressedLength][int: originalLength][compressed payload]. */ private void writeProtocolMessage(byte[] payload) throws IOException { - if (isLocalConnection) { + if (!useCompression) { + out.writeInt(payload.length); out.write(payload); } else { byte[] compressed = Zstd.compress(payload); + out.writeInt(compressed.length); out.writeInt(payload.length); out.write(compressed); } @@ -264,12 +275,23 @@ private void writeProtocolMessage(byte[] payload) throws IOException { * Reads one framed protocol message, decompressing it. */ private byte[] readProtocolMessageFrame() throws IOException { - if (isLocalConnection) { - return in.readAllBytes(); + if (!useCompression) { + int origLength = in.readInt(); + byte[] data = new byte[origLength]; + in.readFully(data); + return data; } else { + int compLength = in.readInt(); int origLength = in.readInt(); - byte[] compData = in.readAllBytes(); - return Zstd.decompress(compData, origLength); + byte[] compData = new byte[compLength]; + in.readFully(compData); + byte[] decompressed = Zstd.decompress(compData, origLength); + + if (decompressed.length != origLength) { + throw new IOException("Decompressed length does not match original length"); + } + + return decompressed; } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java index 7d109e62..23473ec2 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java @@ -1,5 +1,6 @@ package pl.skidam.automodpack_core.protocol; +import io.netty.util.AttributeKey; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; @@ -35,6 +36,8 @@ public class NetUtils { public static final byte END_OF_TRANSMISSION = 0x04; public static final byte ERROR = 0x05; + public static final AttributeKey USE_COMPRESSION = AttributeKey.valueOf("useCompression"); + public static String getFingerprint(X509Certificate cert, String secret) throws CertificateEncodingException { byte[] sharedSecret = secret.getBytes(); byte[] certificate = cert.getEncoded(); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java index 535a32c5..006910e6 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java @@ -5,8 +5,10 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.ssl.SslContext; import io.netty.handler.stream.ChunkedWriteHandler; +import pl.skidam.automodpack_core.protocol.NetUtils; import pl.skidam.automodpack_core.utils.AddressHelpers; +import java.net.InetSocketAddress; import java.util.List; import static pl.skidam.automodpack_core.protocol.NetUtils.*; @@ -40,15 +42,16 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t var handlers = ctx.pipeline().toMap(); handlers.forEach((name, handler) -> ctx.pipeline().remove(handler)); - String address = ctx.channel().remoteAddress().toString(); + InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress(); boolean isLocalConnection = AddressHelpers.isLocal(address); + // Use compression only for non-local connections + ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).set(!isLocalConnection); + // Set up the pipeline for our protocol ctx.pipeline().addLast("tls", sslCtx.newHandler(ctx.alloc())); - if (!isLocalConnection) { - ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); - ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); - } + ctx.pipeline().addLast("zstd-encoder", new ZstdEncoder()); + ctx.pipeline().addLast("zstd-decoder", new ZstdDecoder()); ctx.pipeline().addLast("chunked-write", new ChunkedWriteHandler()); ctx.pipeline().addLast("protocol-msg-decoder", new ProtocolMessageDecoder()); ctx.pipeline().addLast("msg-handler", new ServerMessageHandler()); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java index df0adb83..06559d11 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java @@ -4,6 +4,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; +import pl.skidam.automodpack_core.protocol.NetUtils; import java.util.List; @@ -11,14 +12,46 @@ public class ZstdDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (!ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).get()) { + if (in.readableBytes() < 4) { + return; + } + + int length = in.readInt(); + + if (in.readableBytes() < length) { + in.resetReaderIndex(); + return; + } + + ByteBuf buf = in.readBytes(length); + out.add(buf); + return; + } + + + if (in.readableBytes() < 8) { + return; + } + + int compressedLength = in.readInt(); int originalLength = in.readInt(); - byte[] compressed = new byte[in.readableBytes()]; + if (in.readableBytes() < compressedLength) { + in.resetReaderIndex(); + return; + } + + byte[] compressed = new byte[compressedLength]; in.readBytes(compressed); byte[] decompressed = Zstd.decompress(compressed, originalLength); - ByteBuf decompressedBuf = ctx.alloc().buffer(decompressed.length); + if (decompressed.length != originalLength) { + throw new IllegalStateException("Decompressed length does not match original length"); + } + + ByteBuf decompressedBuf = ctx.alloc().buffer(originalLength); decompressedBuf.writeBytes(decompressed); out.add(decompressedBuf); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java index 57b69956..3e423a8e 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java @@ -4,16 +4,24 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; +import pl.skidam.automodpack_core.protocol.NetUtils; public class ZstdEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { + if (!ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).get()) { + out.writeInt(msg.readableBytes()); + out.writeBytes(msg); + return; + } + byte[] input = new byte[msg.readableBytes()]; msg.readBytes(input); byte[] compressed = Zstd.compress(input); + out.writeInt(compressed.length); out.writeInt(input.length); out.writeBytes(compressed); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java index bb93e573..9c123676 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java @@ -87,11 +87,17 @@ public static String normalizeIp(String ip) { return ip; } - public static boolean isLocal(String ip) { - if (ip == null) { + public static boolean isLocal(InetSocketAddress address) { + if (address == null) { return true; } + if (address.getAddress().isAnyLocalAddress()) { + return true; + } + + String ip = address.getAddress().getHostAddress(); + ip = normalizeIp(ip); String localIp = getLocalIp(); String localIpv6 = getLocalIpv6(); diff --git a/src/main/java/pl/skidam/automodpack/client/audio/AudioManager.java b/src/main/java/pl/skidam/automodpack/client/audio/AudioManager.java index 9899311c..e73420d6 100644 --- a/src/main/java/pl/skidam/automodpack/client/audio/AudioManager.java +++ b/src/main/java/pl/skidam/automodpack/client/audio/AudioManager.java @@ -16,11 +16,11 @@ *//*?}*/ /*? if neoforge {*/ -/*import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.registries.DeferredRegister; import net.neoforged.neoforge.registries.NeoForgeRegistries; import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; -*//*?} elif forge {*/ +/*?} elif forge {*/ /*import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.ForgeRegistries; @@ -51,19 +51,19 @@ public class AudioManager { *//*?}*/ /*? if neoforge {*/ - /*public AudioManager(IEventBus eventBus) { + public AudioManager(IEventBus eventBus) { DeferredRegister SOUND_REGISTER = DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID); SOUND_REGISTER.register(eventBus); WAITING_MUSIC = SOUND_REGISTER.register(WAITING_MUSIC_ID.getPath(),()-> WAITING_MUSIC_EVENT); } -*//*?}*/ +/*?}*/ /*? if fabric {*/ - public AudioManager() { + /*public AudioManager() { SoundEvent waiting_music = register(); WAITING_MUSIC = () -> waiting_music; } -/*?}*/ +*//*?}*/ private SoundEvent register() { Identifier id = Common.id("waiting_music"); diff --git a/src/main/java/pl/skidam/automodpack/init/FabricInit.java b/src/main/java/pl/skidam/automodpack/init/FabricInit.java index 662b55d8..91a93ad3 100644 --- a/src/main/java/pl/skidam/automodpack/init/FabricInit.java +++ b/src/main/java/pl/skidam/automodpack/init/FabricInit.java @@ -1,7 +1,7 @@ package pl.skidam.automodpack.init; /*? if fabric {*/ -import pl.skidam.automodpack.client.ScreenImpl; +/*import pl.skidam.automodpack.client.ScreenImpl; import pl.skidam.automodpack.client.audio.AudioManager; import pl.skidam.automodpack.modpack.Commands; import pl.skidam.automodpack.networking.ModPackets; @@ -9,7 +9,7 @@ import pl.skidam.automodpack_loader_core.screen.ScreenManager; import static pl.skidam.automodpack_core.GlobalVariables.*; -import net.fabricmc.fabric.api.command./*? if <1.19.1 {*/ /*v1 *//*?} else {*/ v2 /*?}*/.CommandRegistrationCallback; +import net.fabricmc.fabric.api.command./^? if <1.19.1 {^/ /^v1 ^//^?} else {^/ v2 /^?}^/.CommandRegistrationCallback; public class FabricInit { @@ -30,11 +30,11 @@ public static void onInitialize() { new AudioManager(); } - CommandRegistrationCallback.EVENT.register((dispatcher, /*? if >=1.19.1 {*/ w, /*?}*/ dedicated) -> { + CommandRegistrationCallback.EVENT.register((dispatcher, /^? if >=1.19.1 {^/ w, /^?}^/ dedicated) -> { Commands.register(dispatcher); }); LOGGER.info("AutoModpack launched! took " + (System.currentTimeMillis() - start) + "ms"); } } -/*?}*/ \ No newline at end of file +*//*?}*/ \ No newline at end of file diff --git a/src/main/java/pl/skidam/automodpack/init/NeoForgeInit.java b/src/main/java/pl/skidam/automodpack/init/NeoForgeInit.java index 3ab19502..78692f2f 100644 --- a/src/main/java/pl/skidam/automodpack/init/NeoForgeInit.java +++ b/src/main/java/pl/skidam/automodpack/init/NeoForgeInit.java @@ -1,9 +1,9 @@ package pl.skidam.automodpack.init; /*? if neoforge {*/ -/*/^? if >1.20.5 {^/ +/*? if >1.20.5 {*/ import net.neoforged.fml.common.EventBusSubscriber; -/^?}^/ +/*?}*/ import pl.skidam.automodpack.client.ScreenImpl; import pl.skidam.automodpack.client.audio.AudioManager; import pl.skidam.automodpack.modpack.Commands; @@ -40,11 +40,11 @@ public NeoForgeInit(IEventBus eventBus) { LOGGER.info("AutoModpack launched! took " + (System.currentTimeMillis() - start) + "ms"); } -/^? if >1.20.5 {^/ +/*? if >1.20.5 {*/ @EventBusSubscriber(modid = MOD_ID) -/^?} else {^/ - /^@Mod.EventBusSubscriber(modid = MOD_ID) -^//^?}^/ +/*?} else {*/ + /*@Mod.EventBusSubscriber(modid = MOD_ID) +*//*?}*/ public static class events { @SubscribeEvent public static void onCommandsRegister(RegisterCommandsEvent event) { @@ -52,4 +52,4 @@ public static void onCommandsRegister(RegisterCommandsEvent event) { } } } -*//*?}*/ +/*?}*/ diff --git a/src/main/java/pl/skidam/automodpack/modpack/Commands.java b/src/main/java/pl/skidam/automodpack/modpack/Commands.java index db41b24a..2f93e8fb 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/Commands.java +++ b/src/main/java/pl/skidam/automodpack/modpack/Commands.java @@ -61,11 +61,11 @@ public static void register(CommandDispatcher dispatcher) { private static int connections(CommandContext serverCommandSourceCommandContext) { Util.getMainWorkerExecutor().execute(() -> { var connections = hostServer.getConnections(); - var secrets = Set.copyOf(connections.values()); + var uniqueSecrets = Set.copyOf(connections.values()); - send(serverCommandSourceCommandContext, String.format("Active connections: %d Unique connections: %d ", connections.size(), secrets.size()), Formatting.YELLOW, false); + send(serverCommandSourceCommandContext, String.format("Active connections: %d Unique connections: %d ", connections.size(), uniqueSecrets.size()), Formatting.YELLOW, false); - for (String secret : secrets) { + for (String secret : uniqueSecrets) { var playerSecretPair = SecretsStore.getHostSecret(secret); if (playerSecretPair == null) continue; diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index 065fe5f6..0b05ba76 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -20,6 +20,8 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.utils.AddressHelpers; +import java.net.InetSocketAddress; + import static pl.skidam.automodpack.networking.ModPackets.DATA; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -92,11 +94,11 @@ public static void handleHandshake(ClientConnection connection, GameProfile prof return; } - String playerIp = connection.getAddress().toString(); + InetSocketAddress playerAddress = (InetSocketAddress) connection.getAddress(); String addressToSend; // If the player is connecting locally, use the local host IP - if (AddressHelpers.isLocal(playerIp)) { + if (AddressHelpers.isLocal(playerAddress)) { addressToSend = serverConfig.hostLocalIp; } else { addressToSend = serverConfig.hostIp; diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 9dcc74c7..054ac0a1 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -9,7 +9,7 @@ plugins { id("dev.kikugie.stonecutter") } -stonecutter active "1.21.1-fabric" /* [SC] DO NOT EDIT */ +stonecutter active "1.21.1-neoforge" /* [SC] DO NOT EDIT */ stonecutter registerChiseled tasks.register("chiseledBuild", stonecutter.chiseled) { group = "project" From 37f1dfdee24f554b3369408e1d110ae19758914d Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 19:21:01 +0100 Subject: [PATCH 20/50] Reinitialize DownloadClient --- .../client/ModpackUpdater.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index ea1990b0..5328fb85 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -300,16 +300,17 @@ public void startUpdate() { // success // or fail and then show the error - downloadManager = new DownloadManager(totalBytesToDownload); - new ScreenManager().download(downloadManager, getModpackName()); - downloadManager.attachDownloadClient(downloadClient); - var refreshedContent = refreshedContentOptional.get(); this.unModifiedSMC = GSON.toJson(refreshedContent); // filter list to only the failed downloads var refreshedFilteredList = refreshedContent.list.stream().filter(item -> hashesToRefresh.containsKey(item.file)).toList(); + downloadManager = new DownloadManager(totalBytesToDownload); + new ScreenManager().download(downloadManager, getModpackName()); + downloadClient = new DownloadClient(modpackAddress, modpackSecret, Math.min(refreshedFilteredList.size(), 5)); + downloadManager.attachDownloadClient(downloadClient); + // TODO try to fetch again from modrinth and curseforge var randomizedList = new LinkedList<>(refreshedFilteredList); @@ -340,8 +341,6 @@ public void startUpdate() { } } - downloadClient.close(); - LOGGER.info("Done, saving {}", modpackContentFile.getFileName().toString()); // Downloads completed From 02c6244fbdcb3d1f6f4d68b3c1d59e7e7b1d5547 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 19:58:06 +0100 Subject: [PATCH 21/50] Don't ever return *previous* key value lol --- .../skidam/automodpack_core/utils/FileInspection.java | 10 +++++++--- .../automodpack_loader_core/client/ModpackUtils.java | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java index 995cd0fb..c596ab18 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java @@ -43,12 +43,14 @@ public static Mod getMod(Path file) { } HashPathPair hashPathPair = new HashPathPair(hash, file); - if (modCache.containsKey(hashPathPair)) + if (modCache.containsKey(hashPathPair)) { return modCache.get(hashPathPair); + } for (Mod mod : GlobalVariables.LOADER_MANAGER.getModList()) { if (hash.equals(mod.hash)) { - return modCache.put(hashPathPair, mod); + modCache.put(hashPathPair, mod); + return mod; } } @@ -60,9 +62,11 @@ public static Mod getMod(Path file) { if (modId != null && modVersion != null && environmentType != null && dependencies != null) { var mod = new Mod(modId, hash, providesIDs, modVersion, file, environmentType, dependencies); - return modCache.put(hashPathPair, mod); + modCache.put(hashPathPair, mod); + return mod; } + GlobalVariables.LOGGER.error("Failed to get mod info for file: {}", file); return null; } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 9492b5ef..1e87d624 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -139,7 +139,6 @@ public static boolean fixNestedMods(List conflictingNestedMo return false; final List standardModIDs = standardModList.stream().map(FileInspection.Mod::modID).toList(); -// LOGGER.info("standardModIDs: {}", standardModIDs); boolean needsRestart = false; for (FileInspection.Mod mod : conflictingNestedMods) { From fbd848007cb6069f2bbc6f494f69b40fc09625a8 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 25 Feb 2025 20:52:12 +0100 Subject: [PATCH 22/50] Fix #328 clean up modpack loader --- .../mods/ModpackLoader15.java | 7 +------ .../mods/ModpackLoader16.java | 7 +------ .../networking/packet/DataC2SPacket.java | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/loader/fabric/15/src/main/java/pl/skidam/automodpack_loader_core_fabric_15/mods/ModpackLoader15.java b/loader/fabric/15/src/main/java/pl/skidam/automodpack_loader_core_fabric_15/mods/ModpackLoader15.java index c6bec9fa..015142bf 100644 --- a/loader/fabric/15/src/main/java/pl/skidam/automodpack_loader_core_fabric_15/mods/ModpackLoader15.java +++ b/loader/fabric/15/src/main/java/pl/skidam/automodpack_loader_core_fabric_15/mods/ModpackLoader15.java @@ -130,12 +130,7 @@ public List getModpackNestedConflicts(Path modpackDir) { List originModIds = new ArrayList<>(); for (ModCandidate mod : conflictingNestedModsImpl) { - String originModId = mod.getParentMods().stream().filter(ModCandidate::isRoot).findFirst().map(ModCandidate::getId).orElse(null); - if (originModId == null) { - LOGGER.error("Why would it be null? {} - {}", mod, mod.getOriginPaths()); - } else { - originModIds.add(originModId); - } + mod.getParentMods().stream().filter(ModCandidate::isRoot).findFirst().map(ModCandidate::getId).ifPresent(originModIds::add); } // These are nested mods which we need to force load from standard mods dir diff --git a/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java b/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java index b8e305e5..794a4fb4 100644 --- a/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java +++ b/loader/fabric/16/src/main/java/pl/skidam/automodpack_loader_core_fabric_16/mods/ModpackLoader16.java @@ -130,12 +130,7 @@ public List getModpackNestedConflicts(Path modpackDir) { List originModIds = new ArrayList<>(); for (ModCandidateImpl mod : conflictingNestedModsImpl) { - String originModId = mod.getParentMods().stream().filter(ModCandidateImpl::isRoot).findFirst().map(ModCandidateImpl::getId).orElse(null); - if (originModId == null) { - LOGGER.error("Why would it be null? {} - {}", mod, mod.getOriginPaths()); - } else { - originModIds.add(originModId); - } + mod.getParentMods().stream().filter(ModCandidateImpl::isRoot).findFirst().map(ModCandidateImpl::getId).ifPresent(originModIds::add); } // These are nested mods which we need to force load from standard mods dir diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index b82d5fa2..7fbbe8d9 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -51,15 +51,13 @@ public static CompletableFuture receive(MinecraftClient minecraft LOGGER.info("Received address packet from server! {} Attached port: {}", addressString, port); } - Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); - boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); - // save secret Secrets.Secret secret = dataPacket.secret; SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); Boolean needsDisconnecting = null; + Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(address, secret); if (optionalServerModpackContent.isPresent()) { @@ -69,13 +67,16 @@ public static CompletableFuture receive(MinecraftClient minecraft disconnectImmediately(handler); new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), address, secret, modpackDir); needsDisconnecting = true; - } else if (selectedModpackChanged) { - disconnectImmediately(handler); - // Its needed since newly selected modpack may not be loaded - new ReLauncher(modpackDir, UpdateType.SELECT).restart(false); - needsDisconnecting = true; } else { - needsDisconnecting = false; + boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); + if (selectedModpackChanged) { + disconnectImmediately(handler); + // Its needed since newly selected modpack may not be loaded + new ReLauncher(modpackDir, UpdateType.SELECT).restart(false); + needsDisconnecting = true; + } else { + needsDisconnecting = false; + } } } From 8976dcd26fe85fa89bcbfc3519e693e1c06a0abd Mon Sep 17 00:00:00 2001 From: skidam Date: Wed, 26 Feb 2025 15:41:09 +0100 Subject: [PATCH 23/50] Small cleanups and always use compression since no noticeable diff was found on local networks --- .../automodpack_core/modpack/ModpackContent.java | 12 ++++++++---- .../automodpack_core/protocol/DownloadClient.java | 7 +++---- .../netty/handler/ProtocolServerHandler.java | 12 ++++++------ .../protocol/netty/handler/ServerMessageHandler.java | 8 ++++---- .../EarlyModLocator.java | 1 - 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java index e70fd24e..d02c5c0c 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java @@ -192,12 +192,16 @@ public void remove(Path file) { } } - // check if file is hostModpackContentFile, serverConfigFile or serverCoreConfigFile + // check if file is inside automodpack Dir or its sub-dirs, unless it's inside hostModpackDir with exception of hostModpackContentFile private boolean isInnerFile(Path file) { Path normalizedFilePath = file.toAbsolutePath().normalize(); - return normalizedFilePath.equals(hostModpackContentFile.toAbsolutePath().normalize()) || - normalizedFilePath.equals(serverConfigFile.toAbsolutePath().normalize()) || - normalizedFilePath.equals(serverCoreConfigFile.toAbsolutePath().normalize()); + boolean isInner = normalizedFilePath.startsWith(automodpackDir.toAbsolutePath().normalize()) && + !normalizedFilePath.startsWith(hostModpackDir.toAbsolutePath().normalize()); + if (!isInner && normalizedFilePath.equals(hostModpackContentFile.toAbsolutePath().normalize())) { + return true; + } + + return isInner; } private Jsons.ModpackContentFields.ModpackContentItem generateContent(final Path file) throws Exception { diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index adc3dcd5..33c5f043 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -1,10 +1,8 @@ package pl.skidam.automodpack_core.protocol; -import pl.skidam.automodpack_core.GlobalVariables; import pl.skidam.automodpack_core.auth.Secrets; import com.github.luben.zstd.Zstd; import pl.skidam.automodpack_core.callbacks.IntCallback; -import pl.skidam.automodpack_core.utils.AddressHelpers; import javax.net.ssl.*; import java.io.*; @@ -88,7 +86,7 @@ public void close() { class Connection { private static final byte PROTOCOL_VERSION = 1; - private final boolean useCompression; // Don't compress local connections + private final boolean useCompression; private final byte[] secretBytes; private final SSLSocket socket; private final DataInputStream in; @@ -153,7 +151,8 @@ public Connection(InetSocketAddress remoteAddress, Secrets.Secret secret) throws throw new IOException("Server certificate validation failed"); } - useCompression = !AddressHelpers.isLocal(remoteAddress); +// useCompression = !AddressHelpers.isLocal(remoteAddress); + useCompression = true; secretBytes = Base64.getUrlDecoder().decode(secret.secret()); // Now use the SSL socket for further communication. diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java index 006910e6..e71ea828 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java @@ -6,9 +6,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.stream.ChunkedWriteHandler; import pl.skidam.automodpack_core.protocol.NetUtils; -import pl.skidam.automodpack_core.utils.AddressHelpers; -import java.net.InetSocketAddress; import java.util.List; import static pl.skidam.automodpack_core.protocol.NetUtils.*; @@ -42,11 +40,13 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t var handlers = ctx.pipeline().toMap(); handlers.forEach((name, handler) -> ctx.pipeline().remove(handler)); - InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress(); - boolean isLocalConnection = AddressHelpers.isLocal(address); +// InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress(); +// boolean isLocalConnection = AddressHelpers.isLocal(address); +// +// // Use compression only for non-local connections +// ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).set(!isLocalConnection); - // Use compression only for non-local connections - ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).set(!isLocalConnection); + ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).set(true); // Set up the pipeline for our protocol ctx.pipeline().addLast("tls", sslCtx.newHandler(ctx.alloc())); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java index 5d4bdb65..f02e4600 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java @@ -28,8 +28,8 @@ public class ServerMessageHandler extends SimpleChannelInboundHandler { + private static final byte PROTOCOL_VERSION = 1; private final Map secretLookup = new HashMap<>(); - private byte clientProtocolVersion = 0; @Override public void handlerRemoved(ChannelHandlerContext ctx) { @@ -38,7 +38,7 @@ public void handlerRemoved(ChannelHandlerContext ctx) { @Override protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) throws Exception { - clientProtocolVersion = msg.getVersion(); + byte clientProtocolVersion = msg.getVersion(); SocketAddress address = ctx.channel().remoteAddress(); // Validate the secret @@ -150,7 +150,7 @@ private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) throws IOExceptio // Send file response header: version, FILE_RESPONSE type, then file size (8 bytes) ByteBuf responseHeader = Unpooled.buffer(1 + 1 + 8); - responseHeader.writeByte(clientProtocolVersion); + responseHeader.writeByte(PROTOCOL_VERSION); responseHeader.writeByte(FILE_RESPONSE_TYPE); responseHeader.writeLong(fileSize); ctx.writeAndFlush(responseHeader); @@ -191,7 +191,7 @@ private void sendError(ChannelHandlerContext ctx, byte version, String errorMess private void sendEOT(ChannelHandlerContext ctx) { ByteBuf eot = Unpooled.buffer(2); - eot.writeByte((byte) 1); + eot.writeByte(PROTOCOL_VERSION); eot.writeByte(END_OF_TRANSMISSION); ctx.writeAndFlush(eot); } diff --git a/loader/forge/fml40/src/main/java/pl/skidam/automodpack_loader_core_forge/EarlyModLocator.java b/loader/forge/fml40/src/main/java/pl/skidam/automodpack_loader_core_forge/EarlyModLocator.java index f8100046..47df4a85 100644 --- a/loader/forge/fml40/src/main/java/pl/skidam/automodpack_loader_core_forge/EarlyModLocator.java +++ b/loader/forge/fml40/src/main/java/pl/skidam/automodpack_loader_core_forge/EarlyModLocator.java @@ -20,7 +20,6 @@ public String name() { return "automodpack_bootstrap"; } - @Override public Stream scanCandidates() { From e20fdfcf37d6a15e3ccf2037aa718ffbcc228c66 Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 16:12:52 +0100 Subject: [PATCH 24/50] Various improvements and beta 25 bump bad commit ik --- core/build.gradle.kts | 1 - .../modpack/ModpackContent.java | 12 +- .../protocol/DownloadClient.java | 113 ++++++++------- .../protocol/netty/NettyServer.java | 9 +- .../netty/handler/ProtocolMessageEncoder.java | 6 - .../utils/AddressHelpers.java | 36 +++-- .../utils/CustomFileUtils.java | 26 +++- gradle.properties | 2 +- .../automodpack_loader_core/Preload.java | 15 +- .../client/ModpackUpdater.java | 75 +++++++--- .../client/ModpackUtils.java | 27 +--- loader/loader-fabric-core.gradle.kts | 1 - loader/loader-forge.gradle.kts | 1 - .../mixin/core/ServerNetworkIoMixin.java | 7 +- .../skidam/automodpack/modpack/Commands.java | 14 +- .../networking/packet/DataC2SPacket.java | 100 ++++++------- .../networking/packet/DataS2CPacket.java | 72 +++++----- .../networking/packet/HandshakeC2SPacket.java | 33 +++-- .../networking/packet/HandshakeS2CPacket.java | 136 +++++++++--------- 19 files changed, 368 insertions(+), 318 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 26178445..58f18ef1 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { implementation("org.apache.logging.log4j:log4j-core:2.20.0") implementation("com.google.code.gson:gson:2.10.1") implementation("io.netty:netty-all:4.1.118.Final") -// implementation("org.bouncycastle:bcprov-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("com.github.luben:zstd-jni:1.5.7-1") implementation("org.tomlj:tomlj:1.1.1") diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java index d02c5c0c..11d7b366 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java @@ -54,11 +54,13 @@ public boolean create() { // host-modpack generation if (MODPACK_DIR != null) { LOGGER.info("Syncing {}...", MODPACK_DIR.getFileName()); - creationFutures.addAll(generateAsync(Files.walk(MODPACK_DIR).toList())); + try (var pathStream = Files.walk(MODPACK_DIR)) { + creationFutures.addAll(generateAsync(pathStream.toList())); - // Wait till finish - creationFutures.forEach((CompletableFuture::join)); - creationFutures.clear(); + // Wait till finish + creationFutures.forEach((CompletableFuture::join)); + creationFutures.clear(); + } } // synced files generation @@ -193,7 +195,7 @@ public void remove(Path file) { } // check if file is inside automodpack Dir or its sub-dirs, unless it's inside hostModpackDir with exception of hostModpackContentFile - private boolean isInnerFile(Path file) { + public static boolean isInnerFile(Path file) { Path normalizedFilePath = file.toAbsolutePath().normalize(); boolean isInner = normalizedFilePath.startsWith(automodpackDir.toAbsolutePath().normalize()) && !normalizedFilePath.startsWith(hostModpackDir.toAbsolutePath().normalize()); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index 33c5f043..6739affe 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -16,6 +16,7 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; +import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; import static pl.skidam.automodpack_core.protocol.NetUtils.*; /** @@ -27,9 +28,9 @@ public class DownloadClient { private final List connections = new ArrayList<>(); - public DownloadClient(InetSocketAddress remoteAddress, Secrets.Secret secret, int poolSize) throws Exception { + public DownloadClient(InetSocketAddress address, Secrets.Secret secret, int poolSize) throws Exception { for (int i = 0; i < poolSize; i++) { - connections.add(new Connection(remoteAddress, secret)); + connections.add(new Connection(address, secret)); } } @@ -102,63 +103,71 @@ public boolean isActive() { * Creates a new connection by first opening a plain TCP socket, * sending the AMMC magic, waiting for the AMOK reply, and then upgrading to TLS. */ - public Connection(InetSocketAddress remoteAddress, Secrets.Secret secret) throws Exception { - // Step 1. Create a plain TCP connection. - Socket plainSocket = new Socket(remoteAddress.getHostName(), remoteAddress.getPort()); - DataOutputStream plainOut = new DataOutputStream(plainSocket.getOutputStream()); - DataInputStream plainIn = new DataInputStream(plainSocket.getInputStream()); - - // Step 2. Send the handshake (AMMC magic) over the plain socket. - plainOut.writeInt(MAGIC_AMMC); - plainOut.flush(); - - // Step 3. Wait for the server’s reply (AMOK magic). - int handshakeResponse = plainIn.readInt(); - if (handshakeResponse != MAGIC_AMOK) { - plainSocket.close(); - throw new IOException("Invalid handshake response from server: " + handshakeResponse); - } + public Connection(InetSocketAddress address, Secrets.Secret secret) throws Exception { + try { + // Step 1. Create a plain TCP connection. + LOGGER.info("Initializing connection to: {}", address); + Socket plainSocket = new Socket(); + plainSocket.connect(address, 15000); + plainSocket.setSoTimeout(15000); + DataOutputStream plainOut = new DataOutputStream(plainSocket.getOutputStream()); + DataInputStream plainIn = new DataInputStream(plainSocket.getInputStream()); + + // Step 2. Send the handshake (AMMC magic) over the plain socket. + plainOut.writeInt(MAGIC_AMMC); + plainOut.flush(); + + // Step 3. Wait for the server’s reply (AMOK magic). + int handshakeResponse = plainIn.readInt(); + if (handshakeResponse != MAGIC_AMOK) { + plainSocket.close(); + throw new IOException("Invalid handshake response from server: " + handshakeResponse); + } - // Step 4. Upgrade the plain socket to TLS using the same underlying connection. - SSLContext context = createSSLContext(); - SSLSocketFactory factory = context.getSocketFactory(); - // The createSocket(Socket, host, port, autoClose) wraps the existing plain socket. - SSLSocket sslSocket = (SSLSocket) factory.createSocket(plainSocket, remoteAddress.getHostName(), remoteAddress.getPort(), true); - sslSocket.setEnabledProtocols(new String[] {"TLSv1.3"}); - sslSocket.setEnabledCipherSuites(new String[] {"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"}); - sslSocket.startHandshake(); - - // Step 5. Perform custom TLS certificate validation. - Certificate[] certs = sslSocket.getSession().getPeerCertificates(); - if (certs == null || certs.length == 0 || certs.length > 3) { - sslSocket.close(); - throw new IOException("Invalid server certificate chain"); - } + // Step 4. Upgrade the plain socket to TLS using the same underlying connection. + SSLContext context = createSSLContext(); + SSLSocketFactory factory = context.getSocketFactory(); + // The createSocket(Socket, host, port, autoClose) wraps the existing plain socket. + SSLSocket sslSocket = (SSLSocket) factory.createSocket(plainSocket, address.getHostName(), address.getPort(), true); + sslSocket.setEnabledProtocols(new String[]{"TLSv1.3"}); + sslSocket.setEnabledCipherSuites(new String[]{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"}); + sslSocket.startHandshake(); + + // Step 5. Perform custom TLS certificate validation. + Certificate[] certs = sslSocket.getSession().getPeerCertificates(); + if (certs == null || certs.length == 0 || certs.length > 3) { + sslSocket.close(); + throw new IOException("Invalid server certificate chain"); + } - boolean validated = false; - for (Certificate cert : certs) { - if (cert instanceof X509Certificate x509Cert) { - String fingerprint = NetUtils.getFingerprint(x509Cert, secret.secret()); - if (fingerprint.equals(secret.fingerprint())) { - validated = true; - break; + boolean validated = false; + for (Certificate cert : certs) { + if (cert instanceof X509Certificate x509Cert) { + String fingerprint = NetUtils.getFingerprint(x509Cert, secret.secret()); + if (fingerprint.equals(secret.fingerprint())) { + validated = true; + break; + } } } - } - if (!validated) { - sslSocket.close(); - throw new IOException("Server certificate validation failed"); - } + if (!validated) { + sslSocket.close(); + throw new IOException("Server certificate validation failed"); + } -// useCompression = !AddressHelpers.isLocal(remoteAddress); - useCompression = true; - secretBytes = Base64.getUrlDecoder().decode(secret.secret()); +// useCompression = !AddressHelpers.isLocal(address); + useCompression = true; + secretBytes = Base64.getUrlDecoder().decode(secret.secret()); - // Now use the SSL socket for further communication. - this.socket = sslSocket; - this.in = new DataInputStream(sslSocket.getInputStream()); - this.out = new DataOutputStream(sslSocket.getOutputStream()); + // Now use the SSL socket for further communication. + this.socket = sslSocket; + this.in = new DataInputStream(sslSocket.getInputStream()); + this.out = new DataOutputStream(sslSocket.getOutputStream()); + LOGGER.info("Connection established with: {}", address); + } catch (Exception e) { + throw new IOException("Failed to establish connection", e); + } } public boolean isBusy() { diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 1a14d8e8..51520a15 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -29,7 +29,6 @@ import static pl.skidam.automodpack_core.GlobalVariables.*; -// TODO: clean up this class public class NettyServer { private final Map connections = Collections.synchronizedMap(new HashMap<>()); private final Map paths = Collections.synchronizedMap(new HashMap<>()); @@ -126,7 +125,6 @@ public Optional start() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(MOD_ID, new ProtocolServerHandler(sslCtx)); - shouldHost = true; } }) .group(eventLoopGroup) @@ -167,9 +165,7 @@ public boolean stop() { return true; } - // TODO: investigate what this method really means xd - // ... why do we rely on it in commands? - public boolean shouldRunInternally() { + public boolean isRunning() { if (serverChannel == null) { return shouldHost; } @@ -186,7 +182,7 @@ public SslContext getSslCtx() { } private boolean canStart() { - if (shouldRunInternally()) { + if (isRunning()) { return false; } @@ -228,6 +224,7 @@ private boolean canStart() { } } + shouldHost = true; LOGGER.info("Starting modpack host server on port {}", serverConfig.hostPort); return true; } diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java index 2bb5ea08..9a8c5969 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolMessageEncoder.java @@ -42,10 +42,4 @@ protected void encode(ChannelHandlerContext ctx, ProtocolMessage msg, ByteBuf ou throw new IllegalArgumentException("Unknown message type: " + msg.getType()); } } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java index 9c123676..f83413c0 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java @@ -87,25 +87,39 @@ public static String normalizeIp(String ip) { return ip; } - public static boolean isLocal(InetSocketAddress address) { + public static InetSocketAddress parse(String address) { + InetSocketAddress socketAddress = null; + try { + int portIndex = address.lastIndexOf(':'); + if (portIndex != -1) { + String host = address.substring(0, portIndex); + String port = address.substring(portIndex + 1); + if (port.matches("\\d+")) { + socketAddress = new InetSocketAddress(host, Integer.parseInt(port)); + } + } + if (socketAddress == null) { + socketAddress = new InetSocketAddress(address, 0); + } + } catch (Exception e) { + LOGGER.error("Error while parsing address", e); + } + + return socketAddress; + } + + public static boolean isLocal(String address) { if (address == null) { return true; } - if (address.getAddress().isAnyLocalAddress()) { + address = normalizeIp(address); + if (address.startsWith("192.168.") || address.startsWith("127.") || address.startsWith("::1") || address.startsWith("0:0:0:0:")) { return true; } - String ip = address.getAddress().getHostAddress(); - - ip = normalizeIp(ip); String localIp = getLocalIp(); String localIpv6 = getLocalIpv6(); - - if (ip.startsWith("192.168.") || ip.startsWith("127.") || ip.startsWith("::1") || ip.startsWith("0:0:0:0:")) { - return true; - } - - return areIpsEqual(ip, localIp) || areIpsEqual(ip, localIpv6); + return areIpsEqual(address, localIp) || areIpsEqual(address, localIpv6); } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java index 9830a726..f7b8d521 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java @@ -6,6 +6,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; @@ -50,15 +52,26 @@ public static Path getPathFromCWD(String path) { // Special for use instead of normal resolve, since it wont work because of the leading slash in file public static Path getPath(Path origin, String path) { - if (path == null) { - return null; + if (origin == null) { + throw new IllegalArgumentException("Origin path must not be null"); + } + if (path == null || path.isBlank()) { + return origin; + } + + path = path.replace('\\', '/'); + + // windows... should fix issues with encoding + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + Charset win1252 = Charset.forName("windows-1252"); + path = new String(path.getBytes(win1252), StandardCharsets.UTF_8); } if (path.startsWith("/")) { - return origin.resolve(path.substring(1)); + path = path.substring(1); } - return origin.resolve(path); + return origin.resolve(path).normalize(); } // our implementation of Files.copy, thanks to usage of RandomAccessFile we can copy files that are in use @@ -350,7 +363,8 @@ public static boolean hashCompare(Path file1, Path file2) { public static boolean isEmptyDirectory(Path parentPath) throws IOException { if (!Files.isDirectory(parentPath)) return false; - List files = Files.list(parentPath).toList(); - return files.isEmpty(); + try (Stream pathStream = Files.list(parentPath)) { + return pathStream.findAny().isEmpty(); + } } } diff --git a/gradle.properties b/gradle.properties index 05f8ef4e..78803e1f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,6 @@ mixin_extras = 0.3.6 mod_id = automodpack mod_name = AutoModpack -mod_version = 4.0.0-beta24 +mod_version = 4.0.0-beta25 mod_group = pl.skidam.automodpack mod_description = Enjoy a seamless modpack installation process and effortless updates with a user-friendly solution that simplifies management, making your gaming experience a breeze. diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java index f0eab703..c13345c5 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java @@ -4,14 +4,11 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.automodpack_core.utils.CustomFileUtils; -import pl.skidam.automodpack_core.utils.FileInspection; -import pl.skidam.automodpack_core.utils.ModpackContentTools; +import pl.skidam.automodpack_core.utils.*; import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.client.ModpackUtils; import pl.skidam.automodpack_loader_core.loader.LoaderManager; import pl.skidam.automodpack_core.loader.LoaderManagerService; -import pl.skidam.automodpack_core.utils.ManifestReader; import pl.skidam.automodpack_loader_core.mods.ModpackLoader; import java.io.IOException; @@ -61,15 +58,7 @@ private void updateAll() { return; } - InetSocketAddress selectedModpackAddress; - - try { - int portIndex = selectedModpackLink.lastIndexOf(":"); - selectedModpackAddress = new InetSocketAddress(selectedModpackLink.substring(0, portIndex), Integer.parseInt(selectedModpackLink.substring(portIndex + 1))); - } catch (Exception e) { - return; - } - + InetSocketAddress selectedModpackAddress = AddressHelpers.parse(selectedModpackLink); Secrets.Secret secret = SecretsStore.getClientSecret(clientConfig.selectedModpack); var optionalLatestModpackContent = ModpackUtils.requestServerModpackContent(selectedModpackAddress, secret); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 5328fb85..5cf103f7 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -15,6 +15,7 @@ import java.net.SocketTimeoutException; import java.nio.file.*; import java.util.*; +import java.util.stream.Stream; import static pl.skidam.automodpack_core.GlobalVariables.*; import static pl.skidam.automodpack_core.config.ConfigTools.GSON; @@ -109,21 +110,29 @@ public void CheckAndLoadModpack() throws Exception { // Load the modpack excluding mods from standard mods directory without need to restart the game if (preload) { - List standardModsHashes = Files.list(MODS_DIR) - .map(CustomFileUtils::getHash) - .filter(Objects::nonNull) - .toList(); - List modpackMods = Files.list(modpackDir.resolve("mods")) - .filter(mod -> { - String modHash = CustomFileUtils.getHash(mod); - - // if its in standard mods directory, we dont want to load it again - boolean isUnique = standardModsHashes.stream().noneMatch(hash -> hash.equals(modHash)); - boolean endsWithJar = mod.toString().endsWith(".jar"); - boolean isFile = mod.toFile().isFile(); - - return isUnique && endsWithJar && isFile; - }).toList(); + List standardModsHashes; + List modpackMods; + + try (Stream standardModsStream = Files.list(MODS_DIR)) { + standardModsHashes = standardModsStream + .map(CustomFileUtils::getHash) + .filter(Objects::nonNull) + .toList(); + } + + try (Stream modpackModsStream = Files.list(modpackDir.resolve("mods"))) { + modpackMods = modpackModsStream + .filter(mod -> { + String modHash = CustomFileUtils.getHash(mod); + + // if its in standard mods directory, we dont want to load it again + boolean isUnique = standardModsHashes.stream().noneMatch(hash -> hash.equals(modHash)); + boolean endsWithJar = mod.toString().endsWith(".jar"); + boolean isFile = mod.toFile().isFile(); + + return isUnique && endsWithJar && isFile; + }).toList(); + } MODPACK_LOADER.loadModpack(modpackMods); return; @@ -359,7 +368,7 @@ public void startUpdate() { for (var download : failedDownloads.entrySet()) { var item = download.getKey(); var urls = download.getValue(); - LOGGER.error("Failed to download: " + item.file + " from " + urls); + LOGGER.error("{}{}", "Failed to download: " + item.file + " from ", urls); failedFiles.append(item.file); } @@ -376,7 +385,7 @@ public void startUpdate() { new ReLauncher(modpackDir, updateType, changelogs).restart(false); } } catch (SocketTimeoutException | ConnectException e) { - LOGGER.error("Modpack host of " + modpackAddress + " is not responding", e); + LOGGER.error("{} is not responding", "Modpack host of " + modpackAddress, e); } catch (InterruptedException e) { LOGGER.info("Interrupted the download"); } catch (Exception e) { @@ -429,10 +438,29 @@ private boolean applyModpack() throws Exception { LOGGER.warn("Found conflicting nested mods: {}", conflictingNestedMods); } - final List modpackMods = Files.list(modpackDir.resolve("mods")).toList(); - final Collection modpackModList = modpackMods.stream().map(FileInspection::getMod).filter(Objects::nonNull).toList(); - final List standardMods = Files.list(MODS_DIR).toList(); - final Collection standardModList = new ArrayList<>(standardMods.stream().map(FileInspection::getMod).filter(Objects::nonNull).toList()); + final List modpackMods; + final Collection modpackModList; + final List standardMods; + final Collection standardModList; + + Path modpackModsDir = modpackDir.resolve("mods"); + if (Files.exists(modpackModsDir)) { + try (Stream modpackModsStream = Files.list(modpackModsDir)) { + modpackMods = modpackModsStream.toList(); + modpackModList = modpackMods.stream().map(FileInspection::getMod).filter(Objects::nonNull).toList(); + } + } else { + modpackModList = List.of(); + } + + if (Files.exists(MODS_DIR)) { + try (Stream standardModsStream = Files.list(MODS_DIR)) { + standardMods = standardModsStream.toList(); + standardModList = new ArrayList<>(standardMods.stream().map(FileInspection::getMod).filter(Objects::nonNull).toList()); + } + } else { + standardModList = new ArrayList<>(); + } boolean needsRestart2 = ModpackUtils.fixNestedMods(conflictingNestedMods, standardModList); Set ignoredFiles = ModpackUtils.getIgnoredWithNested(conflictingNestedMods, filesNotToCopy); @@ -467,7 +495,10 @@ private Set getIgnoredFiles(Set modpackFiles = modpackContent.list.stream().map(modpackContentField -> modpackContentField.file).toList(); - List pathList = Files.walk(modpackDir).toList(); + List pathList; + try (Stream pathStream = Files.walk(modpackDir)) { + pathList = pathStream.toList(); + } Set workaroundMods = workaroundUtil.getWorkaroundMods(modpackContent); Set parentPaths = new HashSet<>(); boolean needsRestart = false; diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 59bb0254..0456133a 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -7,6 +7,7 @@ import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.protocol.DownloadClient; +import pl.skidam.automodpack_core.utils.AddressHelpers; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.utils.FileInspection; import pl.skidam.automodpack_core.utils.ModpackContentTools; @@ -48,17 +49,17 @@ public static boolean isUpdate(Jsons.ModpackContentFields serverModpackContent, } else { Path standardPath = CustomFileUtils.getPathFromCWD(file); if (Files.exists(standardPath) && Objects.equals(serverSHA1, CustomFileUtils.getHash(standardPath))) { - LOGGER.info("File {} already exists on client, coping to modpack", standardPath.getFileName()); + LOGGER.info("File {} already exists on client, coping to modpack", file); try { CustomFileUtils.copyFile(standardPath, path); } catch (IOException e) { e.printStackTrace(); } continue; } else { - LOGGER.info("File does not exists {}", standardPath); + LOGGER.info("File does not exists {} - {}", standardPath, file); return true; } } if (!Objects.equals(serverSHA1, CustomFileUtils.getHash(path))) { - LOGGER.info("File does not match hash {}", path); + LOGGER.info("File does not match hash {} - {}", path, file); return true; } } @@ -269,7 +270,7 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont String installedModpackName = clientConfig.selectedModpack; String installedModpackLink = clientConfig.installedModpacks.get(installedModpackName); - InetSocketAddress installedModpackAddress = new InetSocketAddress(installedModpackLink.split(":")[0], Integer.parseInt(installedModpackLink.split(":")[1])); + InetSocketAddress installedModpackAddress = AddressHelpers.parse(installedModpackLink); String serverModpackName = serverModpackContent.modpackName; if (!serverModpackName.equals(installedModpackName) && !serverModpackName.isEmpty()) { @@ -303,23 +304,7 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m String selectedModpackLink = clientConfig.installedModpacks.get(selectedModpack); // LOGGER.info("Selected modpack link: {}", selectedModpackLink); - InetSocketAddress selectedModpackAddress = null; - try { - int portIndex = selectedModpackLink.lastIndexOf(':'); - if (portIndex != -1) { - String host = selectedModpackLink.substring(0, portIndex); - String port = selectedModpackLink.substring(portIndex + 1); - if (port.matches("\\d+")) { - selectedModpackAddress = new InetSocketAddress(host, Integer.parseInt(port)); - } - } else { - selectedModpackAddress = new InetSocketAddress(selectedModpackLink, 0); - } - } catch (Exception e) { - if (selectedModpackLink != null && !selectedModpackLink.isBlank()) { - LOGGER.error("Error while parsing selected modpack address", e); - } - } + InetSocketAddress selectedModpackAddress = AddressHelpers.parse(selectedModpackLink); // Save current editable files Path selectedModpackDir = modpacksDir.resolve(selectedModpack); diff --git a/loader/loader-fabric-core.gradle.kts b/loader/loader-fabric-core.gradle.kts index b10e3388..6726fa0f 100644 --- a/loader/loader-fabric-core.gradle.kts +++ b/loader/loader-fabric-core.gradle.kts @@ -25,7 +25,6 @@ dependencies { compileOnly("com.google.code.gson:gson:2.10.1") compileOnly("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.tomlj:tomlj:1.1.1") -// implementation("org.bouncycastle:bcprov-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("com.github.luben:zstd-jni:1.5.7-1") diff --git a/loader/loader-forge.gradle.kts b/loader/loader-forge.gradle.kts index 40ecd340..956e4d5d 100644 --- a/loader/loader-forge.gradle.kts +++ b/loader/loader-forge.gradle.kts @@ -37,7 +37,6 @@ dependencies { compileOnly("com.google.code.gson:gson:2.10.1") compileOnly("org.apache.logging.log4j:log4j-core:2.20.0") implementation("org.tomlj:tomlj:1.1.1") -// implementation("org.bouncycastle:bcprov-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("com.github.luben:zstd-jni:1.5.7-1") diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java index 9e7a8670..7b0994af 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/ServerNetworkIoMixin.java @@ -8,8 +8,7 @@ import pl.skidam.automodpack_core.GlobalVariables; import pl.skidam.automodpack_core.protocol.netty.handler.ProtocolServerHandler; -import static pl.skidam.automodpack_core.GlobalVariables.MOD_ID; -import static pl.skidam.automodpack_core.GlobalVariables.hostServer; +import static pl.skidam.automodpack_core.GlobalVariables.*; @Mixin(targets = "net/minecraft/server/ServerNetworkIo$1", priority = 2137) public abstract class ServerNetworkIoMixin { @@ -19,11 +18,11 @@ public abstract class ServerNetworkIoMixin { at = @At("TAIL") ) private void injectAutoModpackHost(Channel channel, CallbackInfo ci) { - if (!GlobalVariables.serverConfig.hostModpackOnMinecraftPort) { + if (!serverConfig.hostModpackOnMinecraftPort) { return; } - if (!GlobalVariables.serverConfig.modpackHost) { + if (!serverConfig.modpackHost) { return; } diff --git a/src/main/java/pl/skidam/automodpack/modpack/Commands.java b/src/main/java/pl/skidam/automodpack/modpack/Commands.java index 2f93e8fb..35a88b69 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/Commands.java +++ b/src/main/java/pl/skidam/automodpack/modpack/Commands.java @@ -97,10 +97,10 @@ private static int reload(CommandContext context) { private static int startModpackHost(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { - if (!hostServer.shouldRunInternally()) { + if (!hostServer.isRunning()) { send(context, "Starting modpack hosting...", Formatting.YELLOW, true); hostServer.start(); - if (hostServer.shouldRunInternally()) { + if (hostServer.isRunning()) { send(context, "Modpack hosting started!", Formatting.GREEN, true); } else { send(context, "Couldn't start server!", Formatting.RED, true); @@ -115,7 +115,7 @@ private static int startModpackHost(CommandContext context) private static int stopModpackHost(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { - if (hostServer.shouldRunInternally()) { + if (hostServer.isRunning()) { send(context, "Stopping modpack hosting...", Formatting.RED, true); if (hostServer.stop()) { send(context, "Modpack hosting stopped!", Formatting.RED, true); @@ -133,7 +133,7 @@ private static int stopModpackHost(CommandContext context) private static int restartModpackHost(CommandContext context) { Util.getMainWorkerExecutor().execute(() -> { send(context, "Restarting modpack hosting...", Formatting.YELLOW, true); - boolean needStop = hostServer.shouldRunInternally(); + boolean needStop = hostServer.isRunning(); boolean stopped = false; if (needStop) { stopped = hostServer.stop(); @@ -143,7 +143,7 @@ private static int restartModpackHost(CommandContext contex send(context, "Couldn't restart server!", Formatting.RED, true); } else { hostServer.start(); - if (hostServer.shouldRunInternally()) { + if (hostServer.isRunning()) { send(context, "Modpack hosting restarted!", Formatting.GREEN, true); } else { send(context, "Couldn't restart server!", Formatting.RED, true); @@ -156,8 +156,8 @@ private static int restartModpackHost(CommandContext contex private static int modpackHostAbout(CommandContext context) { - Formatting statusColor = hostServer.shouldRunInternally() ? Formatting.GREEN : Formatting.RED; - String status = hostServer.shouldRunInternally() ? "running" : "not running"; + Formatting statusColor = hostServer.isRunning() ? Formatting.GREEN : Formatting.RED; + String status = hostServer.isRunning() ? "running" : "not running"; send(context, "Modpack hosting status", Formatting.GREEN, status, statusColor, false); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index 7fbbe8d9..1252718d 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -23,68 +23,72 @@ public class DataC2SPacket { public static CompletableFuture receive(MinecraftClient minecraftClient, ClientLoginNetworkHandler handler, PacketByteBuf buf) { - String serverResponse = buf.readString(Short.MAX_VALUE); - - DataPacket dataPacket = DataPacket.fromJson(serverResponse); - String packetAddress = dataPacket.address; - Integer packetPort = dataPacket.port; - boolean modRequired = dataPacket.modRequired; + try { + String serverResponse = buf.readString(Short.MAX_VALUE); + + DataPacket dataPacket = DataPacket.fromJson(serverResponse); + String packetAddress = dataPacket.address; + Integer packetPort = dataPacket.port; + boolean modRequired = dataPacket.modRequired; + + if (modRequired) { + // TODO set screen to refreshed danger screen which will ask user to install modpack with two options + // 1. Disconnect and install modpack + // 2. Dont disconnect and join server + } - if (modRequired) { - // TODO set screen to refreshed danger screen which will ask user to install modpack with two options - // 1. Disconnect and install modpack - // 2. Dont disconnect and join server - } + InetSocketAddress address = (InetSocketAddress) ((ClientLoginNetworkHandlerAccessor) handler).getConnection().getAddress(); - InetSocketAddress address = (InetSocketAddress) ((ClientLoginNetworkHandlerAccessor) handler).getConnection().getAddress(); - - if (packetAddress.isBlank()) { - LOGGER.info("Address from connected server: {}:{}", address.getAddress().getHostName(), address.getPort()); - } else if (packetPort != null) { - address = new InetSocketAddress(packetAddress, packetPort); - LOGGER.info("Received address packet from server! {}:{}", packetAddress, packetPort); - } else { - var portIndex = packetAddress.lastIndexOf(':'); - var port = portIndex == -1 ? 0 : Integer.parseInt(packetAddress.substring(portIndex + 1)); - var addressString = portIndex == -1 ? packetAddress : packetAddress.substring(0, portIndex); - address = new InetSocketAddress(addressString, port); - LOGGER.info("Received address packet from server! {} Attached port: {}", addressString, port); - } + if (packetAddress.isBlank()) { + LOGGER.info("Address from connected server: {}:{}", address.getAddress().getHostAddress(), address.getPort()); + } else if (packetPort != null) { + address = new InetSocketAddress(packetAddress, packetPort); + LOGGER.info("Received address packet from server! {}:{}", packetAddress, packetPort); + } else { + var portIndex = packetAddress.lastIndexOf(':'); + var port = portIndex == -1 ? 0 : Integer.parseInt(packetAddress.substring(portIndex + 1)); + var addressString = portIndex == -1 ? packetAddress : packetAddress.substring(0, portIndex); + address = new InetSocketAddress(addressString, port); + LOGGER.info("Received address packet from server! {} Attached port: {}", addressString, port); + } - // save secret - Secrets.Secret secret = dataPacket.secret; - SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + // save secret + Secrets.Secret secret = dataPacket.secret; + SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); - Boolean needsDisconnecting = null; + Boolean needsDisconnecting = null; - Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); - var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(address, secret); + Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); + var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(address, secret); - if (optionalServerModpackContent.isPresent()) { - boolean update = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); + if (optionalServerModpackContent.isPresent()) { + boolean update = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); - if (update) { - disconnectImmediately(handler); - new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), address, secret, modpackDir); - needsDisconnecting = true; - } else { - boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); - if (selectedModpackChanged) { + if (update) { disconnectImmediately(handler); - // Its needed since newly selected modpack may not be loaded - new ReLauncher(modpackDir, UpdateType.SELECT).restart(false); + new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), address, secret, modpackDir); needsDisconnecting = true; } else { - needsDisconnecting = false; + boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); + if (selectedModpackChanged) { + disconnectImmediately(handler); + // Its needed since newly selected modpack may not be loaded + new ReLauncher(modpackDir, UpdateType.SELECT).restart(false); + needsDisconnecting = true; + } else { + needsDisconnecting = false; + } } } - } - PacketByteBuf response = new PacketByteBuf(Unpooled.buffer()); - response.writeString(String.valueOf(needsDisconnecting), Short.MAX_VALUE); - - return CompletableFuture.completedFuture(response); + PacketByteBuf response = new PacketByteBuf(Unpooled.buffer()); + response.writeString(String.valueOf(needsDisconnecting), Short.MAX_VALUE); + return CompletableFuture.completedFuture(response); + } catch (Exception e) { + LOGGER.error("Error while handling data packet", e); + return CompletableFuture.completedFuture(new PacketByteBuf(Unpooled.buffer())); + } } private static void disconnectImmediately(ClientLoginNetworkHandler clientLoginNetworkHandler) { diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataS2CPacket.java index f8a3264e..062b20c2 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataS2CPacket.java @@ -18,44 +18,48 @@ public class DataS2CPacket { public static void receive(MinecraftServer server, ServerLoginNetworkHandler handler, boolean understood, PacketByteBuf buf, ServerLoginNetworking.LoginSynchronizer loginSynchronizer, PacketSender sender) { - GameProfile profile = ((ServerLoginNetworkHandlerAccessor) handler).getGameProfile(); - - String clientHasUpdate = buf.readString(Short.MAX_VALUE); - - if ("true".equals(clientHasUpdate)) { // disconnect - LOGGER.warn("{} has not installed modpack", profile.getName()); - Text reason = VersionedText.literal("[AutoModpack] Install/Update modpack to join"); - ClientConnection connection = ((ServerLoginNetworkHandlerAccessor) handler).getConnection(); - connection.send(new LoginDisconnectS2CPacket(reason)); - connection.disconnect(reason); - } else if ("false".equals(clientHasUpdate)) { - LOGGER.info("{} has installed whole modpack", profile.getName()); - } else { - Text reason = VersionedText.literal("[AutoModpack] Host server error. Please contact server administrator to check the server logs!"); - ClientConnection connection = ((ServerLoginNetworkHandlerAccessor) handler).getConnection(); - connection.send(new LoginDisconnectS2CPacket(reason)); - connection.disconnect(reason); - - LOGGER.error("Host server error. AutoModpack host server is down or server is not configured correctly"); - - if (serverConfig.hostModpackOnMinecraftPort) { - LOGGER.warn("You are hosting Http server on the minecraft port."); - LOGGER.warn("However client can't access it, try making `hostIp` and `hostLocalIp` blank in the server config."); - LOGGER.warn("If that doesn't work, follow the steps bellow."); - LOGGER.warn(""); + try { + GameProfile profile = ((ServerLoginNetworkHandlerAccessor) handler).getGameProfile(); + + String clientHasUpdate = buf.readString(Short.MAX_VALUE); + + if ("true".equals(clientHasUpdate)) { // disconnect + LOGGER.warn("{} has not installed modpack", profile.getName()); + Text reason = VersionedText.literal("[AutoModpack] Install/Update modpack to join"); + ClientConnection connection = ((ServerLoginNetworkHandlerAccessor) handler).getConnection(); + connection.send(new LoginDisconnectS2CPacket(reason)); + connection.disconnect(reason); + } else if ("false".equals(clientHasUpdate)) { + LOGGER.info("{} has installed whole modpack", profile.getName()); } else { - LOGGER.warn("Please check if AutoModpack host server (TCP) port '{}' is forwarded / opened correctly", GlobalVariables.serverConfig.hostPort); - LOGGER.warn(""); - } + Text reason = VersionedText.literal("[AutoModpack] Host server error. Please contact server administrator to check the server logs!"); + ClientConnection connection = ((ServerLoginNetworkHandlerAccessor) handler).getConnection(); + connection.send(new LoginDisconnectS2CPacket(reason)); + connection.disconnect(reason); + + LOGGER.error("Host server error. AutoModpack host server is down or server is not configured correctly"); + + if (serverConfig.hostModpackOnMinecraftPort) { + LOGGER.warn("You are hosting Http server on the minecraft port."); + LOGGER.warn("However client can't access it, try making `hostIp` and `hostLocalIp` blank in the server config."); + LOGGER.warn("If that doesn't work, follow the steps bellow."); + LOGGER.warn(""); + } else { + LOGGER.warn("Please check if AutoModpack host server (TCP) port '{}' is forwarded / opened correctly", GlobalVariables.serverConfig.hostPort); + LOGGER.warn(""); + } - LOGGER.warn("Make sure that host IP '{}' and host local IP '{}' are correct in the config file!", GlobalVariables.serverConfig.hostIp, GlobalVariables.serverConfig.hostLocalIp); - LOGGER.warn("host IP should be an ip which are players outside of server network connecting to and host local IP should be an ip which are players inside of server network connecting to"); - LOGGER.warn("It can be Ip or a correctly set domain"); - LOGGER.warn("If you need, change port in config file, forward / open it and restart server"); + LOGGER.warn("Make sure that host IP '{}' and host local IP '{}' are correct in the config file!", GlobalVariables.serverConfig.hostIp, GlobalVariables.serverConfig.hostLocalIp); + LOGGER.warn("host IP should be an ip which are players outside of server network connecting to and host local IP should be an ip which are players inside of server network connecting to"); + LOGGER.warn("It can be Ip or a correctly set domain"); + LOGGER.warn("If you need, change port in config file, forward / open it and restart server"); - if (serverConfig.reverseProxy) { - LOGGER.error("Turn off reverseProxy in config, if you don't actually use it!"); + if (serverConfig.reverseProxy) { + LOGGER.error("Turn off reverseProxy in config, if you don't actually use it!"); + } } + } catch (Exception e) { + LOGGER.error("Error while handling DataS2CPacket", e); } } } diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeC2SPacket.java index 9215a2e1..d4ee10d2 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeC2SPacket.java @@ -18,25 +18,30 @@ public class HandshakeC2SPacket { public static CompletableFuture receive(MinecraftClient client, ClientLoginNetworkHandler handler, PacketByteBuf buf) { - String serverResponse = buf.readString(Short.MAX_VALUE); + try { + String serverResponse = buf.readString(Short.MAX_VALUE); - HandshakePacket serverHandshakePacket = HandshakePacket.fromJson(serverResponse); + HandshakePacket serverHandshakePacket = HandshakePacket.fromJson(serverResponse); - String loader = LOADER_MANAGER.getPlatformType().toString().toLowerCase(); + String loader = LOADER_MANAGER.getPlatformType().toString().toLowerCase(); - PacketByteBuf outBuf = new PacketByteBuf(Unpooled.buffer()); - HandshakePacket clientHandshakePacket = new HandshakePacket(List.of(loader), AM_VERSION, MC_VERSION); - outBuf.writeString(clientHandshakePacket.toJson(), Short.MAX_VALUE); + PacketByteBuf outBuf = new PacketByteBuf(Unpooled.buffer()); + HandshakePacket clientHandshakePacket = new HandshakePacket(List.of(loader), AM_VERSION, MC_VERSION); + outBuf.writeString(clientHandshakePacket.toJson(), Short.MAX_VALUE); - if (serverHandshakePacket.equals(clientHandshakePacket) || (serverHandshakePacket.loaders.contains(loader) && serverHandshakePacket.amVersion.equals(AM_VERSION))) { - LOGGER.info("Versions match " + serverHandshakePacket.amVersion); - } else { - LOGGER.warn("Versions mismatch " + serverHandshakePacket.amVersion); - LOGGER.info("Trying to change automodpack version to the version required by server..."); - updateMod(handler, serverHandshakePacket.amVersion, serverHandshakePacket.mcVersion); - } + if (serverHandshakePacket.equals(clientHandshakePacket) || (serverHandshakePacket.loaders.contains(loader) && serverHandshakePacket.amVersion.equals(AM_VERSION))) { + LOGGER.info("Versions match " + serverHandshakePacket.amVersion); + } else { + LOGGER.warn("Versions mismatch " + serverHandshakePacket.amVersion); + LOGGER.info("Trying to change automodpack version to the version required by server..."); + updateMod(handler, serverHandshakePacket.amVersion, serverHandshakePacket.mcVersion); + } - return CompletableFuture.completedFuture(outBuf); + return CompletableFuture.completedFuture(outBuf); + } catch (Exception e) { + LOGGER.error("Error while handling HandshakeC2SPacket", e); + return CompletableFuture.completedFuture(new PacketByteBuf(Unpooled.buffer())); + } } private static void updateMod(ClientLoginNetworkHandler handler, String serverAMVersion, String serverMCVersion) { diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index 0b05ba76..4801d805 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -20,8 +20,6 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.utils.AddressHelpers; -import java.net.InetSocketAddress; - import static pl.skidam.automodpack.networking.ModPackets.DATA; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -60,87 +58,95 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han } public static void handleHandshake(ClientConnection connection, GameProfile profile, int minecraftServerPort, PacketByteBuf buf, PacketSender packetSender) { - LOGGER.info("{} has installed AutoModpack.", profile.getName()); + try { + LOGGER.info("{} has installed AutoModpack.", profile.getName()); - String clientResponse = buf.readString(Short.MAX_VALUE); - HandshakePacket clientHandshakePacket = HandshakePacket.fromJson(clientResponse); + String clientResponse = buf.readString(Short.MAX_VALUE); + HandshakePacket clientHandshakePacket = HandshakePacket.fromJson(clientResponse); - boolean isAcceptedLoader = false; - for (String loader : serverConfig.acceptedLoaders) { - if (clientHandshakePacket.loaders.contains(loader)) { - isAcceptedLoader = true; - break; + boolean isAcceptedLoader = false; + for (String loader : serverConfig.acceptedLoaders) { + if (clientHandshakePacket.loaders.contains(loader)) { + isAcceptedLoader = true; + break; + } } - } - if (!isAcceptedLoader || !clientHandshakePacket.amVersion.equals(AM_VERSION)) { - Text reason = VersionedText.literal("AutoModpack version mismatch! Install " + AM_VERSION + " version of AutoModpack mod for " + LOADER_MANAGER.getPlatformType().toString().toLowerCase() + " to play on this server!"); - if (isClientVersionHigher(clientHandshakePacket.amVersion)) { - reason = VersionedText.literal("You are using a more recent version of AutoModpack than the server. Please contact the server administrator to update the AutoModpack mod."); + if (!isAcceptedLoader || !clientHandshakePacket.amVersion.equals(AM_VERSION)) { + Text reason = VersionedText.literal("AutoModpack version mismatch! Install " + AM_VERSION + " version of AutoModpack mod for " + LOADER_MANAGER.getPlatformType().toString().toLowerCase() + " to play on this server!"); + if (isClientVersionHigher(clientHandshakePacket.amVersion)) { + reason = VersionedText.literal("You are using a more recent version of AutoModpack than the server. Please contact the server administrator to update the AutoModpack mod."); + } + connection.send(new LoginDisconnectS2CPacket(reason)); + connection.disconnect(reason); + return; } - connection.send(new LoginDisconnectS2CPacket(reason)); - connection.disconnect(reason); - return; - } - if (!hostServer.shouldRunInternally()) { - return; - } + if (!hostServer.isRunning()) { + LOGGER.info("Host server is not running. Modpack will not be sent to {}", profile.getName()); + return; + } - if (modpack.isGenerating()) { - Text reason = VersionedText.literal("AutoModapck is generating modpack. Please wait a moment and try again."); - connection.send(new LoginDisconnectS2CPacket(reason)); - connection.disconnect(reason); - return; - } + if (modpack.isGenerating()) { + Text reason = VersionedText.literal("AutoModapck is generating modpack. Please wait a moment and try again."); + connection.send(new LoginDisconnectS2CPacket(reason)); + connection.disconnect(reason); + return; + } - InetSocketAddress playerAddress = (InetSocketAddress) connection.getAddress(); - String addressToSend; + String playerAddress = connection.getAddress().toString(); + String addressToSend; - // If the player is connecting locally, use the local host IP - if (AddressHelpers.isLocal(playerAddress)) { - addressToSend = serverConfig.hostLocalIp; - } else { - addressToSend = serverConfig.hostIp; - } + // If the player is connecting locally, use the local host IP + if (AddressHelpers.isLocal(playerAddress)) { + addressToSend = serverConfig.hostLocalIp; + } else { + addressToSend = serverConfig.hostIp; + } - // now we know player is authenticated, packets are encrypted and player is whitelisted - // regenerate unique secret - Secrets.Secret secret = Secrets.generateSecret(); - SecretsStore.saveHostSecret(profile.getId().toString(), secret); + // now we know player is authenticated, packets are encrypted and player is whitelisted + // regenerate unique secret + Secrets.Secret secret = Secrets.generateSecret(); + SecretsStore.saveHostSecret(profile.getId().toString(), secret); - // We send empty string if hostIp/hostLocalIp is not specified in server config. Client will use ip by which it connected to the server in first place. - DataPacket dataPacket = new DataPacket(addressToSend, null, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); + // We send empty string if hostIp/hostLocalIp is not specified in server config. Client will use ip by which it connected to the server in first place. + DataPacket dataPacket = new DataPacket(addressToSend, null, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); - if (serverConfig.reverseProxy) { - // With reverse proxy we dont append port to the link, it should be already included in the link - // But we need to check if the port is set in the config, since that's where modpack is actually hosted - if (serverConfig.hostPort == -1 && !serverConfig.hostModpackOnMinecraftPort) { - LOGGER.error("Reverse proxy is enabled but host port is not set in config! Please set it manually."); - } + if (serverConfig.reverseProxy) { + // With reverse proxy we dont append port to the link, it should be already included in the link + // But we need to check if the port is set in the config, since that's where modpack is actually hosted + if (serverConfig.hostPort == -1 && !serverConfig.hostModpackOnMinecraftPort) { + LOGGER.error("Reverse proxy is enabled but host port is not set in config! Please set it manually."); + } - LOGGER.info("Sending {} modpack url: {}", profile.getName(), addressToSend); - } else { // Append server port - int portToSend; - if (serverConfig.hostModpackOnMinecraftPort) { - portToSend = minecraftServerPort; - } else { - portToSend = serverConfig.hostPort; + LOGGER.info("Sending {} modpack url: {}", profile.getName(), addressToSend); + } else { // Append server port + int portToSend; + if (serverConfig.hostModpackOnMinecraftPort) { + portToSend = minecraftServerPort; + } else { + portToSend = serverConfig.hostPort; + + if (serverConfig.hostPort == -1) { + LOGGER.error("Host port is not set in config! Please set it manually."); + } + } - if (serverConfig.hostPort == -1) { - LOGGER.error("Host port is not set in config! Please set it manually."); + if (!addressToSend.isBlank()) { + LOGGER.info("Sending {} modpack url: {}:{}", profile.getName(), addressToSend, portToSend); } + dataPacket = new DataPacket(addressToSend, portToSend, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); } - LOGGER.info("Sending {} modpack url: {}:{}", profile.getName(), addressToSend, portToSend); - dataPacket = new DataPacket(addressToSend, portToSend, serverConfig.modpackName, secret, serverConfig.requireAutoModpackOnClient); - } - - String packetContentJson = dataPacket.toJson(); + String packetContentJson = dataPacket.toJson(); - PacketByteBuf outBuf = new PacketByteBuf(Unpooled.buffer()); - outBuf.writeString(packetContentJson, Short.MAX_VALUE); - packetSender.sendPacket(DATA, outBuf); + PacketByteBuf outBuf = new PacketByteBuf(Unpooled.buffer()); + outBuf.writeString(packetContentJson, Short.MAX_VALUE); + packetSender.sendPacket(DATA, outBuf); + LOGGER.info("Sent data packet to {} {}", profile.getName(), dataPacket); + } catch (Exception e) { + LOGGER.error("Error while handling handshake for {}", profile.getName(), e); + } } From b2da86842f6f4208570d99d267e31286244f1772 Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 17:03:16 +0100 Subject: [PATCH 25/50] Enhance error handling and logging; improve address parsing and file name validation --- .../protocol/DownloadClient.java | 4 +-- .../utils/AddressHelpers.java | 1 + .../utils/FileInspection.java | 26 ++++++++++++++----- .../client/ModpackUpdater.java | 25 ++++++++++-------- .../client/ModpackUtils.java | 8 ++---- .../networking/packet/HandshakeS2CPacket.java | 1 - 6 files changed, 39 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index 6739affe..603af819 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -106,7 +106,7 @@ public boolean isActive() { public Connection(InetSocketAddress address, Secrets.Secret secret) throws Exception { try { // Step 1. Create a plain TCP connection. - LOGGER.info("Initializing connection to: {}", address); + LOGGER.debug("Initializing connection to: {}", address.getHostString()); Socket plainSocket = new Socket(); plainSocket.connect(address, 15000); plainSocket.setSoTimeout(15000); @@ -164,7 +164,7 @@ public Connection(InetSocketAddress address, Secrets.Secret secret) throws Excep this.socket = sslSocket; this.in = new DataInputStream(sslSocket.getInputStream()); this.out = new DataOutputStream(sslSocket.getOutputStream()); - LOGGER.info("Connection established with: {}", address); + LOGGER.debug("Connection established with: {}", address.getHostString()); } catch (Exception e) { throw new IOException("Failed to establish connection", e); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java index f83413c0..493ca6e3 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/AddressHelpers.java @@ -88,6 +88,7 @@ public static String normalizeIp(String ip) { } public static InetSocketAddress parse(String address) { + if (address == null) return null; InetSocketAddress socketAddress = null; try { int portIndex = address.lastIndexOf(':'); diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java index c596ab18..7968e950 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java @@ -22,6 +22,8 @@ import java.util.zip.ZipException; import java.util.zip.ZipFile; +import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; + public class FileInspection { public static boolean isMod(Path file) { @@ -38,7 +40,7 @@ public static Mod getMod(Path file) { String hash = CustomFileUtils.getHash(file); if (hash == null) { - GlobalVariables.LOGGER.error("Failed to get hash for file: {}", file); + LOGGER.error("Failed to get hash for file: {}", file); return null; } @@ -66,7 +68,7 @@ public static Mod getMod(Path file) { return mod; } - GlobalVariables.LOGGER.error("Failed to get mod info for file: {}", file); + LOGGER.debug("Failed to get mod info for file: {}", file); return null; } @@ -245,7 +247,7 @@ private static Object getModInfo(Path file, String infoType) { private static Object getModInfoFromToml(BufferedReader reader, String infoType, Path file) { try { TomlParseResult result = Toml.parse(reader); - result.errors().forEach(error -> GlobalVariables.LOGGER.error(error.toString())); + result.errors().forEach(error -> LOGGER.error(error.toString())); TomlArray modsArray = result.getArray("mods"); if (modsArray == null) { @@ -389,7 +391,7 @@ private static Object getModInfoFromJson(BufferedReader reader, Gson gson, Strin return infoType.equals("version") || infoType.equals("modId") || infoType.equals("environment") ? null : Set.of(); } - private static final String forbiddenChars = "\\/:*\"<>|!?."; + private static final String forbiddenChars = "\\/:*\"<>|!?&%$;=+"; public static boolean isInValidFileName(String fileName) { // Check for each forbidden character in the file name @@ -399,14 +401,26 @@ public static boolean isInValidFileName(String fileName) { } } + for (char c : fileName.toCharArray()) { + if (c < 32 || c == 127) { + return true; + } + } + // Check if the file name is empty or just contains whitespace return fileName.trim().isEmpty(); } public static String fixFileName(String fileName) { // Replace forbidden characters with underscores - for (char c : forbiddenChars.toCharArray()) { - fileName = fileName.replace(c, '-'); + for (char c : fileName.toCharArray()) { + if (c < 32 || c == 127) { + fileName = fileName.replace(c, '-'); + } + + if (forbiddenChars.indexOf(c) != -1) { + fileName = fileName.replace(c, '-'); + } } // Remove leading and trailing whitespace diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 5cf103f7..1395e66b 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -111,7 +111,7 @@ public void CheckAndLoadModpack() throws Exception { // Load the modpack excluding mods from standard mods directory without need to restart the game if (preload) { List standardModsHashes; - List modpackMods; + List modpackMods = List.of(); try (Stream standardModsStream = Files.list(MODS_DIR)) { standardModsHashes = standardModsStream @@ -120,18 +120,21 @@ public void CheckAndLoadModpack() throws Exception { .toList(); } - try (Stream modpackModsStream = Files.list(modpackDir.resolve("mods"))) { - modpackMods = modpackModsStream - .filter(mod -> { - String modHash = CustomFileUtils.getHash(mod); + Path modpackModsDir = modpackDir.resolve("mods"); + if (Files.exists(modpackModsDir)) { + try (Stream modpackModsStream = Files.list(modpackModsDir)) { + modpackMods = modpackModsStream + .filter(mod -> { + String modHash = CustomFileUtils.getHash(mod); - // if its in standard mods directory, we dont want to load it again - boolean isUnique = standardModsHashes.stream().noneMatch(hash -> hash.equals(modHash)); - boolean endsWithJar = mod.toString().endsWith(".jar"); - boolean isFile = mod.toFile().isFile(); + // if its in standard mods directory, we dont want to load it again + boolean isUnique = standardModsHashes.stream().noneMatch(hash -> hash.equals(modHash)); + boolean endsWithJar = mod.toString().endsWith(".jar"); + boolean isFile = mod.toFile().isFile(); - return isUnique && endsWithJar && isFile; - }).toList(); + return isUnique && endsWithJar && isFile; + }).toList(); + } } MODPACK_LOADER.loadModpack(modpackMods); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 10339be5..85950bba 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -302,7 +302,6 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m String selectedModpack = clientConfig.selectedModpack; String selectedModpackLink = clientConfig.installedModpacks.get(selectedModpack); -// LOGGER.info("Selected modpack link: {}", selectedModpackLink); InetSocketAddress selectedModpackAddress = AddressHelpers.parse(selectedModpackLink); @@ -327,9 +326,6 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m ConfigTools.save(clientConfigFile, clientConfig); ModpackUtils.addModpackToList(modpackToSelect, modpackAddressToSelect); -// LOGGER.warn("modpackToSelect: {}, selectedModpack: {}", modpackToSelect, modpackToSelect); -// LOGGER.warn("modpackAddressToSelect: {}, selectedModpackAddress: {}", modpackAddressToSelect, selectedModpackAddress); - return !Objects.equals(modpackToSelect, selectedModpack) || !Objects.equals(modpackAddressToSelect, selectedModpackAddress); } @@ -352,7 +348,7 @@ public static void addModpackToList(String modpackName, InetSocketAddress addres } Map modpacks = new HashMap<>(clientConfig.installedModpacks); - String addressString = address.getAddress().getHostAddress() + ":" + address.getPort(); + String addressString = address.getHostString() + ":" + address.getPort(); modpacks.put(modpackName, addressString); clientConfig.installedModpacks = modpacks; @@ -362,7 +358,7 @@ public static void addModpackToList(String modpackName, InetSocketAddress addres // Returns modpack name formatted for path or url if server doesn't provide modpack name public static Path getModpackPath(InetSocketAddress address, String modpackName) { - String strAddress = address.getAddress().getHostAddress() + ":" + address.getPort(); + String strAddress = address.getHostString() + ":" + address.getPort(); String correctedName = strAddress; if (FileInspection.isInValidFileName(strAddress)) { diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index 4801d805..9b3cb472 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -143,7 +143,6 @@ public static void handleHandshake(ClientConnection connection, GameProfile prof PacketByteBuf outBuf = new PacketByteBuf(Unpooled.buffer()); outBuf.writeString(packetContentJson, Short.MAX_VALUE); packetSender.sendPacket(DATA, outBuf); - LOGGER.info("Sent data packet to {} {}", profile.getName(), dataPacket); } catch (Exception e) { LOGGER.error("Error while handling handshake for {}", profile.getName(), e); } From 974671612705c729dd915c20cc0ae68e3ae5511b Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 17:58:44 +0100 Subject: [PATCH 26/50] Remove console output for server start --- .../pl/skidam/automodpack_core/protocol/netty/NettyServer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 51520a15..70b68d50 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -131,8 +131,6 @@ protected void initChannel(SocketChannel ch) throws Exception { .localAddress(address, port) .bind() .syncUninterruptibly(); - - System.out.println("Netty file server started on port " + port); } catch (Exception e) { LOGGER.error("Failed to start Netty server", e); return Optional.empty(); From aea18b286848818781c056fa3351284e7d81655c Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 18:40:09 +0100 Subject: [PATCH 27/50] Fix error screen messages --- .../pl/skidam/automodpack/client/ScreenImpl.java | 5 ++--- .../skidam/automodpack/client/ui/ErrorScreen.java | 14 ++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java b/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java index d0692fe4..3f108f06 100644 --- a/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java +++ b/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java @@ -13,7 +13,6 @@ import pl.skidam.automodpack_loader_core.utils.UpdateType; import java.nio.file.Path; -import java.util.Arrays; import java.util.Optional; public class ScreenImpl implements ScreenService { @@ -97,8 +96,8 @@ public static void danger(Object parent, Object modpackUpdaterInstance) { Screens.setScreen(new DangerScreen((Screen) parent, (ModpackUpdater) modpackUpdaterInstance)); } - public static void error(String... error) { - Screens.setScreen(new ErrorScreen(Arrays.toString(error))); + public static void error(String... errors) { + Screens.setScreen(new ErrorScreen(errors)); } public static void title() { diff --git a/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java index d0c58ace..fdce592c 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java @@ -9,12 +9,12 @@ import pl.skidam.automodpack.client.ui.versioned.VersionedText; public class ErrorScreen extends VersionedScreen { - private final String[] errorMessage; + private final String[] errorMessages; private ButtonWidget backButton; - public ErrorScreen(String... errorMessage) { + public ErrorScreen(String... errorMessages) { super(VersionedText.literal("ErrorScreen")); - this.errorMessage = errorMessage; + this.errorMessages = errorMessages; if (AudioManager.isMusicPlaying()) { AudioManager.stopMusic(); @@ -39,11 +39,9 @@ private void initWidgets() { @Override public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, float delta) { - // Something went wrong! - // TODO fix this text - drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.literal("[AutoModpack] Error! ").append(VersionedText.translatable("automodpack.error").formatted(Formatting.RED)), this.width / 2, this.height / 2 - 40, 16777215); - for (int i = 0; i < this.errorMessage.length; i++) { - drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.translatable(this.errorMessage[i]), this.width / 2, this.height / 2 - 20 + i * 10, 14687790); + drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.literal("[AutoModpack] Error! ").append(VersionedText.translatable("automodpack.error").formatted(Formatting.RED)), this.width / 2, this.height / 2 - 50, 16777215); + for (int i = 0; i < this.errorMessages.length; i++) { + drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.translatable(this.errorMessages[i]), this.width / 2, this.height / 2 - 20 + i * 14, 14687790); } } From c919eb78f60196bd2b2bf2b79185629e0e7adb74 Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 18:41:46 +0100 Subject: [PATCH 28/50] 12 > 14 --- src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java index fdce592c..52c0b947 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java @@ -41,7 +41,7 @@ private void initWidgets() { public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, float delta) { drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.literal("[AutoModpack] Error! ").append(VersionedText.translatable("automodpack.error").formatted(Formatting.RED)), this.width / 2, this.height / 2 - 50, 16777215); for (int i = 0; i < this.errorMessages.length; i++) { - drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.translatable(this.errorMessages[i]), this.width / 2, this.height / 2 - 20 + i * 14, 14687790); + drawCenteredTextWithShadow(matrices, this.textRenderer, VersionedText.translatable(this.errorMessages[i]), this.width / 2, this.height / 2 - 20 + i * 12, 14687790); } } From 984d9a8c5d6ba73c57a9334a6f5fda7a68dfbdd3 Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 18:59:05 +0100 Subject: [PATCH 29/50] Remove address comparison --- .../skidam/automodpack_loader_core/client/ModpackUtils.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 85950bba..736a839c 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -299,11 +299,7 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont // Returns true if value changed public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress modpackAddressToSelect, Set newDownloadedFiles) { final String modpackToSelect = modpackDirToSelect.getFileName().toString(); - String selectedModpack = clientConfig.selectedModpack; - String selectedModpackLink = clientConfig.installedModpacks.get(selectedModpack); - - InetSocketAddress selectedModpackAddress = AddressHelpers.parse(selectedModpackLink); // Save current editable files Path selectedModpackDir = modpacksDir.resolve(selectedModpack); @@ -326,7 +322,7 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m ConfigTools.save(clientConfigFile, clientConfig); ModpackUtils.addModpackToList(modpackToSelect, modpackAddressToSelect); - return !Objects.equals(modpackToSelect, selectedModpack) || !Objects.equals(modpackAddressToSelect, selectedModpackAddress); + return !Objects.equals(modpackToSelect, selectedModpack); } public static void removeModpackFromList(String modpackName) { From 0d2c0ebd2688f5bc9217a8bed0a9749fdbada389 Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 27 Feb 2025 19:49:00 +0100 Subject: [PATCH 30/50] sanity stuff --- .../netty/handler/ServerMessageHandler.java | 22 +++++++++++++---- .../utils/CustomFileUtils.java | 24 ++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java index f02e4600..30d106fd 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java @@ -160,13 +160,27 @@ private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) throws IOExceptio return; } - // Stream the file using ChunkedFile (chunk size set to 131072 bytes = 128 KB) - suitable value for zstd try { RandomAccessFile raf = new RandomAccessFile(path.toFile(), "r"); - ChunkedFile chunkedFile = new ChunkedFile(raf, 0, raf.length(), 131072); - ctx.writeAndFlush(chunkedFile).addListener((ChannelFutureListener) future -> sendEOT(ctx)); + ChunkedFile chunkedFile = new ChunkedFile(raf, 0, raf.length(), 131072); // 128 KB chunk size - good for zstd + ctx.writeAndFlush(chunkedFile).addListener((ChannelFutureListener) future -> { + try { + if (future.isSuccess()) { + sendEOT(ctx); + } else { + sendError(ctx, PROTOCOL_VERSION, "File transfer error: " + future.cause().getMessage()); + } + } finally { // Always close resources + try { + chunkedFile.close(); + raf.close(); + } catch (IOException e) { + LOGGER.error("Error closing file resources", e); + } + } + }); } catch (IOException e) { - sendError(ctx, (byte) 1, "File transfer error: " + e.getMessage()); + sendError(ctx, PROTOCOL_VERSION, "File transfer error: " + e.getMessage()); } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java index 74620c92..4bf55742 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java @@ -215,13 +215,8 @@ public static void dummyIT(Path file) { } public static String getHash(Path file) { - if (!Files.exists(file)) { - return null; - } - try { - if (!Files.isRegularFile(file)) - return null; + if (!Files.isRegularFile(file)) return null; MessageDigest digest = MessageDigest.getInstance("SHA-1"); try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), "r")) { @@ -233,6 +228,23 @@ public static String getHash(Path file) { } byte[] hashBytes = digest.digest(); return convertBytesToHex(hashBytes); + } catch (UnsupportedOperationException e) { + try { // yes... its awful + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + try (var is = Files.newInputStream(file)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + } + + byte[] hashBytes = digest.digest(); + return convertBytesToHex(hashBytes); + } catch (Exception ex) { + e.printStackTrace(); + ex.printStackTrace(); + } } catch (Exception e) { LOGGER.error("Failed to get hash of file: {}", file, e); } From 9e480c82b27ba28d21a1e7fb729800004d038b15 Mon Sep 17 00:00:00 2001 From: skidam Date: Fri, 28 Feb 2025 08:00:26 +0100 Subject: [PATCH 31/50] Improve mod detection fixes #250 --- .../utils/FileInspection.java | 25 ++++++++++++++++++- .../utils/ManifestReader.java | 11 -------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java index 7968e950..14ce64df 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java @@ -27,7 +27,7 @@ public class FileInspection { public static boolean isMod(Path file) { - return getModID(file) != null || hasSpecificServices(file); + return getModID(file) != null || hasSpecificServices(file) || isFMLMod(file); } public record Mod(String modID, String hash, Collection providesIDs, String modVersion, Path modPath, LoaderManagerService.EnvironmentType environmentType, Collection dependencies) {} @@ -131,6 +131,29 @@ public static boolean hasSpecificServices(Path file) { return false; } + // Check for /META-INF/MANIFEST.MF, if FMLModType entry exists + public static boolean isFMLMod(Path file) { + if (!file.getFileName().toString().endsWith(".jar") || !Files.exists(file)) { + return false; + } + + try (ZipFile zipFile = new ZipFile(file.toFile())) { + ZipEntry entry = zipFile.getEntry("META-INF/MANIFEST.MF"); + if (entry == null) { + return false; + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(zipFile.getInputStream(entry)))) { + return reader.lines().anyMatch(line -> line.startsWith("FMLModType:")); + } + } catch (IOException e) { + LOGGER.error("Error reading manifest for {}: {}", file, e.getMessage()); + } + + return false; + } + public static boolean isModCompatible(Path file) { if (!file.getFileName().toString().endsWith(".jar") || !Files.exists(file)) { return false; diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/ManifestReader.java b/core/src/main/java/pl/skidam/automodpack_core/utils/ManifestReader.java index 798b00a8..3eeb073d 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/ManifestReader.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/ManifestReader.java @@ -1,6 +1,5 @@ package pl.skidam.automodpack_core.utils; -import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.jar.Attributes; @@ -27,14 +26,4 @@ public static String getAutoModpackVersion() { throw new RuntimeException("Couldn't find AutoModpack version in manifest file."); } - - public static String readForgeModVersion(InputStream fileStream) { - try { - Manifest manifest = new Manifest(fileStream); - Attributes mainAttributes = manifest.getMainAttributes(); - return mainAttributes.getValue("Implementation-Version"); - } catch (Exception e) { - throw new RuntimeException(e); - } - } } From 46feb2e583b62cf5a54654ae4201d3fb61ee1a5f Mon Sep 17 00:00:00 2001 From: skidam Date: Fri, 28 Feb 2025 08:01:07 +0100 Subject: [PATCH 32/50] Make sure to save secret for correct modpack --- .../pl/skidam/automodpack_core/auth/SecretsStore.java | 2 +- .../automodpack/networking/packet/DataC2SPacket.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java index 1302216f..3c01c8d2 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java @@ -41,7 +41,7 @@ public Secrets.Secret get(String key) { public void save(String key, Secrets.Secret secret) { if (key == null || key.isBlank() || secret == null) - return; + throw new IllegalArgumentException("Key cannot be null or blank"); load(); cache.put(key, secret); if (db == null) { diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index 1252718d..9d3dba5e 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -52,10 +52,7 @@ public static CompletableFuture receive(MinecraftClient minecraft LOGGER.info("Received address packet from server! {} Attached port: {}", addressString, port); } - // save secret Secrets.Secret secret = dataPacket.secret; - SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); - Boolean needsDisconnecting = null; Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); @@ -65,14 +62,15 @@ public static CompletableFuture receive(MinecraftClient minecraft boolean update = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); if (update) { + SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); disconnectImmediately(handler); new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), address, secret, modpackDir); needsDisconnecting = true; } else { boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); if (selectedModpackChanged) { + SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); disconnectImmediately(handler); - // Its needed since newly selected modpack may not be loaded new ReLauncher(modpackDir, UpdateType.SELECT).restart(false); needsDisconnecting = true; } else { @@ -81,6 +79,8 @@ public static CompletableFuture receive(MinecraftClient minecraft } } + SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + PacketByteBuf response = new PacketByteBuf(Unpooled.buffer()); response.writeString(String.valueOf(needsDisconnecting), Short.MAX_VALUE); From 7533e8fae3403f12ac10b7a101cf3aa872e6413f Mon Sep 17 00:00:00 2001 From: skidam Date: Fri, 28 Feb 2025 09:31:19 +0100 Subject: [PATCH 33/50] Move stuff around to make 1.18 fabric work --- .github/workflows/build.yml | 4 ++- .../skidam/automodpack_core/auth/Secrets.java | 33 ++++++++++++++++++- .../automodpack_core/auth/SecretsStore.java | 4 +-- .../automodpack_core/protocol/NetUtils.java | 3 -- .../protocol/netty/NettyServer.java | 2 ++ .../netty/handler/ProtocolServerHandler.java | 4 +-- .../protocol/netty/handler/ZstdDecoder.java | 4 +-- .../protocol/netty/handler/ZstdEncoder.java | 4 +-- gradle.properties | 4 +-- .../client/ModpackUpdater.java | 2 ++ .../networking/packet/DataC2SPacket.java | 12 ++++--- 11 files changed, 57 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 206858a8..b2a39e93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,9 @@ jobs: chmod +x gradlew if [ -z "${{ inputs.target_subproject }}" ]; then echo "Building all subprojects" - ./gradlew chiseledBuild + ./gradlew clean + ./gradlew chiseledBuild -x mergeJars + ./gradlew mergeJars else args=$(echo "${{ inputs.target_subproject }}" | tr ',' '\n' | sed 's/$/:build/' | paste -sd ' ') echo "Building with arguments=$args" diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java index 2bf9d9a4..6c97b92f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/Secrets.java @@ -10,7 +10,38 @@ import static pl.skidam.automodpack_core.GlobalVariables.*; public class Secrets { - public record Secret(String secret, String fingerprint, Long timestamp) { } + public static class Secret { // unfortunately has to be a class instead of record because of older gson version in 1.18 mc + private String secret; // and these also can't be final + private String fingerprint; + private Long timestamp; + + public Secret(String secret, String fingerprint, Long timestamp) { + this.secret = secret; + this.fingerprint = fingerprint; + this.timestamp = timestamp; + } + + public String secret() { + return secret; + } + + public String fingerprint() { + return fingerprint; + } + + public Long timestamp() { + return timestamp; + } + + @Override + public String toString() { + return "Secret{" + + "secret='" + secret + '\'' + + ", fingerprint='" + fingerprint + '\'' + + ", timestamp=" + timestamp + + '}'; + } + } public static Secret generateSecret() { SecureRandom random = new SecureRandom(); diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java index 3c01c8d2..115ad091 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java @@ -40,8 +40,8 @@ public Secrets.Secret get(String key) { } public void save(String key, Secrets.Secret secret) { - if (key == null || key.isBlank() || secret == null) - throw new IllegalArgumentException("Key cannot be null or blank"); + if (key == null || key.isBlank() || secret == null || secret.secret().isBlank()) + throw new IllegalArgumentException("Key or secret cannot be null or blank"); load(); cache.put(key, secret); if (db == null) { diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java index 23473ec2..7d109e62 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java @@ -1,6 +1,5 @@ package pl.skidam.automodpack_core.protocol; -import io.netty.util.AttributeKey; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; @@ -36,8 +35,6 @@ public class NetUtils { public static final byte END_OF_TRANSMISSION = 0x04; public static final byte ERROR = 0x05; - public static final AttributeKey USE_COMPRESSION = AttributeKey.valueOf("useCompression"); - public static String getFingerprint(X509Certificate cert, String secret) throws CertificateEncodingException { byte[] sharedSecret = secret.getBytes(); byte[] certificate = cert.getEncoded(); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 70b68d50..0b8ac7a6 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -11,6 +11,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; +import io.netty.util.AttributeKey; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.protocol.NetUtils; import pl.skidam.automodpack_core.protocol.netty.handler.ProtocolServerHandler; @@ -30,6 +31,7 @@ import static pl.skidam.automodpack_core.GlobalVariables.*; public class NettyServer { + public static final AttributeKey USE_COMPRESSION = AttributeKey.valueOf("useCompression"); private final Map connections = Collections.synchronizedMap(new HashMap<>()); private final Map paths = Collections.synchronizedMap(new HashMap<>()); private ChannelFuture serverChannel; diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java index e71ea828..179038ca 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ProtocolServerHandler.java @@ -5,7 +5,7 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.ssl.SslContext; import io.netty.handler.stream.ChunkedWriteHandler; -import pl.skidam.automodpack_core.protocol.NetUtils; +import pl.skidam.automodpack_core.protocol.netty.NettyServer; import java.util.List; @@ -46,7 +46,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t // // Use compression only for non-local connections // ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).set(!isLocalConnection); - ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).set(true); + ctx.pipeline().channel().attr(NettyServer.USE_COMPRESSION).set(true); // Set up the pipeline for our protocol ctx.pipeline().addLast("tls", sslCtx.newHandler(ctx.alloc())); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java index 06559d11..7d5e491e 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdDecoder.java @@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; -import pl.skidam.automodpack_core.protocol.NetUtils; +import pl.skidam.automodpack_core.protocol.netty.NettyServer; import java.util.List; @@ -12,7 +12,7 @@ public class ZstdDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - if (!ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).get()) { + if (!ctx.pipeline().channel().attr(NettyServer.USE_COMPRESSION).get()) { if (in.readableBytes() < 4) { return; } diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java index 3e423a8e..7f1d9b76 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ZstdEncoder.java @@ -4,13 +4,13 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import pl.skidam.automodpack_core.protocol.NetUtils; +import pl.skidam.automodpack_core.protocol.netty.NettyServer; public class ZstdEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { - if (!ctx.pipeline().channel().attr(NetUtils.USE_COMPRESSION).get()) { + if (!ctx.pipeline().channel().attr(NettyServer.USE_COMPRESSION).get()) { out.writeInt(msg.readableBytes()); out.writeBytes(msg); return; diff --git a/gradle.properties b/gradle.properties index 78803e1f..e039b74f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.jvmargs = -Xmx6G -# Run clean task before building or turn off parallel builds -# org.gradle.parallel = true +# Run clean task before building if doesn't work +org.gradle.parallel = false org.gradle.caching = true org.gradle.caching.debug = false org.gradle.configureondemand = true diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 1395e66b..7c817970 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -1,6 +1,7 @@ package pl.skidam.automodpack_loader_core.client; import pl.skidam.automodpack_core.auth.Secrets; +import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.protocol.DownloadClient; @@ -400,6 +401,7 @@ public void startUpdate() { // returns true if restart is required private boolean applyModpack() throws Exception { ModpackUtils.selectModpack(modpackDir, modpackAddress, newDownloadedFiles); + SecretsStore.saveClientSecret(clientConfig.selectedModpack, modpackSecret); Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(modpackContentFile); if (modpackContent == null) { diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index 9d3dba5e..f713bfaf 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -29,6 +29,8 @@ public static CompletableFuture receive(MinecraftClient minecraft DataPacket dataPacket = DataPacket.fromJson(serverResponse); String packetAddress = dataPacket.address; Integer packetPort = dataPacket.port; + String modpackName = dataPacket.modpackName; + Secrets.Secret secret = dataPacket.secret; boolean modRequired = dataPacket.modRequired; if (modRequired) { @@ -52,17 +54,15 @@ public static CompletableFuture receive(MinecraftClient minecraft LOGGER.info("Received address packet from server! {} Attached port: {}", addressString, port); } - Secrets.Secret secret = dataPacket.secret; Boolean needsDisconnecting = null; - Path modpackDir = ModpackUtils.getModpackPath(address, dataPacket.modpackName); + Path modpackDir = ModpackUtils.getModpackPath(address, modpackName); var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(address, secret); if (optionalServerModpackContent.isPresent()) { boolean update = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); if (update) { - SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); disconnectImmediately(handler); new ModpackUpdater().prepareUpdate(optionalServerModpackContent.get(), address, secret, modpackDir); needsDisconnecting = true; @@ -79,7 +79,9 @@ public static CompletableFuture receive(MinecraftClient minecraft } } - SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + if (clientConfig.selectedModpack != null && !clientConfig.selectedModpack.isBlank()) { + SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + } PacketByteBuf response = new PacketByteBuf(Unpooled.buffer()); response.writeString(String.valueOf(needsDisconnecting), Short.MAX_VALUE); @@ -87,6 +89,8 @@ public static CompletableFuture receive(MinecraftClient minecraft return CompletableFuture.completedFuture(response); } catch (Exception e) { LOGGER.error("Error while handling data packet", e); + PacketByteBuf response = new PacketByteBuf(Unpooled.buffer()); + response.writeString("null", Short.MAX_VALUE); return CompletableFuture.completedFuture(new PacketByteBuf(Unpooled.buffer())); } } From 41fc77286c2b44c6d07b067ef79846534d08ff45 Mon Sep 17 00:00:00 2001 From: skidam Date: Fri, 28 Feb 2025 11:05:15 +0100 Subject: [PATCH 34/50] Force utf8 on inputstream ref: #319 --- .../pl/skidam/automodpack_core/utils/CustomFileUtils.java | 8 -------- .../automodpack_loader_core/client/ModpackUtils.java | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java index 4bf55742..98c6f189 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/CustomFileUtils.java @@ -6,8 +6,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; @@ -61,12 +59,6 @@ public static Path getPath(Path origin, String path) { path = path.replace('\\', '/'); - // windows... should fix issues with encoding - if (System.getProperty("os.name").toLowerCase().contains("windows")) { - Charset win1252 = Charset.forName("windows-1252"); - path = new String(path.getBytes(win1252), StandardCharsets.UTF_8); - } - if (path.startsWith("/")) { path = path.substring(1); } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 736a839c..8aa3ce2d 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -14,6 +14,7 @@ import java.io.*; import java.net.*; +import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; @@ -439,7 +440,6 @@ public static Optional refreshServerModpackContent(I } public static Optional parseStreamToModpack(List rawBytes) { - String response = null; // get list of bytes[] to one byte[] object @@ -451,7 +451,7 @@ public static Optional parseStreamToModpack(List Date: Fri, 28 Feb 2025 19:52:38 +0100 Subject: [PATCH 35/50] Use temp file instead ref: #319 --- .../pl/skidam/automodpack_core/GlobalVariables.java | 1 + .../automodpack_core/protocol/DownloadClient.java | 3 ++- .../automodpack_loader_core/client/ModpackUtils.java | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java index 0a381797..823b3ad0 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java +++ b/core/src/main/java/pl/skidam/automodpack_core/GlobalVariables.java @@ -44,6 +44,7 @@ public class GlobalVariables { // Client + public static final Path modpackContentTempFile = automodpackDir.resolve("automodpack-content.json.temp"); public static final Path clientConfigFile = automodpackDir.resolve("automodpack-client.json"); public static final Path clientSecretsFile = privateDir.resolve("automodpack-client-secrets.json"); public static final Path modpacksDir = automodpackDir.resolve("modpacks"); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index 603af819..dff80425 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -17,6 +17,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; +import static pl.skidam.automodpack_core.GlobalVariables.modpackContentTempFile; import static pl.skidam.automodpack_core.protocol.NetUtils.*; /** @@ -234,7 +235,7 @@ public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { byte[] payload = baos.toByteArray(); writeProtocolMessage(payload); - return readFileResponse(null, null); + return readFileResponse(modpackContentTempFile, null); } catch (Exception e) { exception = e; throw new CompletionException(e); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 8aa3ce2d..d93adbfe 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -394,10 +394,14 @@ public static Optional requestServerModpackContent(I DownloadClient client = null; try { client = new DownloadClient(address, secret, 1); - var future = client.downloadFile(new byte[0], null, null); + var future = client.downloadFile(new byte[0], modpackContentTempFile, null); var result = future.get(); if (result instanceof List list) { return parseStreamToModpack((List) list); + } else if (result instanceof Path path) { + var content = Optional.ofNullable(ConfigTools.loadModpackContent(path)); + Files.deleteIfExists(path); + return content; } return Optional.empty(); @@ -426,6 +430,10 @@ public static Optional refreshServerModpackContent(I var result = future.get(); if (result instanceof List list) { return parseStreamToModpack((List) list); + } else if (result instanceof Path path) { + var content = Optional.ofNullable(ConfigTools.loadModpackContent(path)); + Files.deleteIfExists(path); + return content; } return Optional.empty(); From 6e577709379e807c99815b436931e980115ba448 Mon Sep 17 00:00:00 2001 From: skidam Date: Sat, 1 Mar 2025 10:47:08 +0100 Subject: [PATCH 36/50] Throw out the parsing method add more logs ref: #319 --- .../automodpack_core/config/ConfigTools.java | 4 +- .../protocol/DownloadClient.java | 39 +++--- .../client/ModpackUpdater.java | 3 +- .../client/ModpackUtils.java | 116 ++++-------------- .../utils/DownloadManager.java | 2 +- 5 files changed, 46 insertions(+), 118 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java index 86a44405..9b82deeb 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java @@ -99,11 +99,11 @@ public static Jsons.ModpackContentFields loadModpackContent(Path modpackContentF try { if (Files.isRegularFile(modpackContentFile)) { String json = Files.readString(modpackContentFile); + LOGGER.warn("Reading modpack content from file: {} - len: {}, Json: {}", modpackContentFile.toAbsolutePath().normalize(), Files.size(modpackContentFile), json); return GSON.fromJson(json, Jsons.ModpackContentFields.class); } } catch (Exception e) { - LOGGER.error("Couldn't load modpack content!"); - e.printStackTrace(); + LOGGER.error("Couldn't load modpack content! {}", modpackContentFile.toAbsolutePath().normalize(), e); } return null; } diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java index dff80425..e2cafd6f 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/DownloadClient.java @@ -17,7 +17,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; -import static pl.skidam.automodpack_core.GlobalVariables.modpackContentTempFile; import static pl.skidam.automodpack_core.protocol.NetUtils.*; /** @@ -55,7 +54,7 @@ private synchronized Connection getFreeConnection() { * Downloads a file identified by its SHA-1 hash to the given destination. * Returns a CompletableFuture that completes when the download finishes. */ - public CompletableFuture downloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { + public CompletableFuture downloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { Connection conn = getFreeConnection(); return conn.sendDownloadFile(fileHash, destination, chunkCallback); } @@ -63,9 +62,9 @@ public CompletableFuture downloadFile(byte[] fileHash, Path destination, /** * Sends a refresh request with the given file hashes. */ - public CompletableFuture requestRefresh(byte[][] fileHashes) { + public CompletableFuture requestRefresh(byte[][] fileHashes, Path destination) { Connection conn = getFreeConnection(); - return conn.sendRefreshRequest(fileHashes); + return conn.sendRefreshRequest(fileHashes, destination); } /** @@ -182,7 +181,11 @@ public void setBusy(boolean value) { /** * Sends a file request over this connection. */ - public CompletableFuture sendDownloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { + public CompletableFuture sendDownloadFile(byte[] fileHash, Path destination, IntCallback chunkCallback) { + if (destination == null) { + throw new IllegalArgumentException("Destination cannot be null"); + } + return CompletableFuture.supplyAsync(() -> { Exception exception = null; try { @@ -212,7 +215,7 @@ public CompletableFuture sendDownloadFile(byte[] fileHash, Path destinat /** * Sends a refresh request over this connection. */ - public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { + public CompletableFuture sendRefreshRequest(byte[][] fileHashes, Path destination) { return CompletableFuture.supplyAsync(() -> { Exception exception = null; try { @@ -235,7 +238,7 @@ public CompletableFuture sendRefreshRequest(byte[][] fileHashes) { byte[] payload = baos.toByteArray(); writeProtocolMessage(payload); - return readFileResponse(modpackContentTempFile, null); + return readFileResponse(destination, null); } catch (Exception e) { exception = e; throw new CompletionException(e); @@ -311,7 +314,7 @@ private byte[] readProtocolMessageFrame() throws IOException { * - One or more data frames containing file data until the total file size is reached. * - A final frame: [protocolVersion][END_OF_TRANSMISSION] */ - private Object readFileResponse(Path destination, IntCallback chunkCallback) throws IOException { + private Path readFileResponse(Path destination, IntCallback chunkCallback) throws IOException { // Header frame byte[] headerFrame = readProtocolMessageFrame(); try (DataInputStream headerIn = new DataInputStream(new ByteArrayInputStream(headerFrame))) { @@ -326,16 +329,15 @@ private Object readFileResponse(Path destination, IntCallback chunkCallback) thr } long receivedBytes = 0; - OutputStream fos = (destination != null) ? new FileOutputStream(destination.toFile()) : null; - List rawData = (fos == null) ? new LinkedList<>() : null; + OutputStream fos = new FileOutputStream(destination.toFile()) ; if (messageType == END_OF_TRANSMISSION) { - if (fos != null) fos.close(); - return (rawData != null) ? rawData : destination; + fos.close(); + return destination; } if (messageType != FILE_RESPONSE_TYPE) { - if (fos != null) fos.close(); + fos.close(); throw new IOException("Unexpected message type: " + messageType); } @@ -346,12 +348,7 @@ private Object readFileResponse(Path destination, IntCallback chunkCallback) thr byte[] dataFrame = readProtocolMessageFrame(); int toWrite = Math.min(dataFrame.length, (int)(expectedFileSize - receivedBytes)); - if (fos != null) { - fos.write(dataFrame, 0, toWrite); - } else { - byte[] chunk = Arrays.copyOfRange(dataFrame, 0, toWrite); - rawData.add(chunk); - } + fos.write(dataFrame, 0, toWrite); receivedBytes += toWrite; if (chunkCallback != null) { @@ -359,7 +356,7 @@ private Object readFileResponse(Path destination, IntCallback chunkCallback) thr } } - if (fos != null) fos.close(); + fos.close(); // Read EOT frame byte[] eotFrame = readProtocolMessageFrame(); @@ -373,7 +370,7 @@ private Object readFileResponse(Path destination, IntCallback chunkCallback) thr } } - return (rawData != null) ? rawData : destination; + return destination; } } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 7c817970..a7a91fb9 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -405,8 +405,7 @@ private boolean applyModpack() throws Exception { Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(modpackContentFile); if (modpackContent == null) { - LOGGER.error("Modpack content is null"); - return false; + throw new IllegalStateException("Failed to load modpack content"); // Something gone very wrong... } if (serverModpackContent != null) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index d93adbfe..8f6403b2 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -1,8 +1,5 @@ package pl.skidam.automodpack_loader_core.client; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; @@ -14,11 +11,11 @@ import java.io.*; import java.net.*; -import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; +import java.util.concurrent.Future; +import java.util.function.Function; -import static pl.skidam.automodpack_core.config.ConfigTools.GSON; import static pl.skidam.automodpack_core.GlobalVariables.*; public class ModpackUtils { @@ -383,60 +380,41 @@ public static Path getModpackPath(InetSocketAddress address, String modpackName) } public static Optional requestServerModpackContent(InetSocketAddress address, Secrets.Secret secret) { - - if (secret == null) - return Optional.empty(); - - if (address == null) - throw new IllegalArgumentException("Address is null"); - - - DownloadClient client = null; - try { - client = new DownloadClient(address, secret, 1); - var future = client.downloadFile(new byte[0], modpackContentTempFile, null); - var result = future.get(); - if (result instanceof List list) { - return parseStreamToModpack((List) list); - } else if (result instanceof Path path) { - var content = Optional.ofNullable(ConfigTools.loadModpackContent(path)); - Files.deleteIfExists(path); - return content; - } - - return Optional.empty(); - } catch (Exception e) { - LOGGER.error("Error while getting server modpack content", e); - } finally { - if (client != null) - client.close(); - } - - return Optional.empty(); + return fetchModpackContent(address, secret, + (client) -> client.downloadFile(new byte[0], modpackContentTempFile, null), + "Fetched"); } public static Optional refreshServerModpackContent(InetSocketAddress address, Secrets.Secret secret, byte[][] fileHashes) { + return fetchModpackContent(address, secret, + (client) -> client.requestRefresh(fileHashes, modpackContentTempFile), + "Re-fetched"); + } + + private static Optional fetchModpackContent(InetSocketAddress address, Secrets.Secret secret, Function> operation, String fetchType) { if (secret == null) return Optional.empty(); - if (address == null) throw new IllegalArgumentException("Address is null"); - DownloadClient client = null; try { client = new DownloadClient(address, secret, 1); - var future = client.requestRefresh(fileHashes); - var result = future.get(); - if (result instanceof List list) { - return parseStreamToModpack((List) list); - } else if (result instanceof Path path) { - var content = Optional.ofNullable(ConfigTools.loadModpackContent(path)); - Files.deleteIfExists(path); - return content; + var future = operation.apply(client); + Path path = future.get(); + var content = Optional.ofNullable(ConfigTools.loadModpackContent(path)); + LOGGER.info("{} the modpack content: {} - {}", fetchType, path.toAbsolutePath().normalize(), content); + if (path.toAbsolutePath().normalize().equals(modpackContentTempFile.toAbsolutePath().normalize())) { + if (Files.deleteIfExists(path)) { + LOGGER.info("Deleted temporary modpack content file: {}", path); + } } - return Optional.empty(); + if (content.isPresent() && potentiallyMalicious(content.get())) { + return Optional.empty(); + } + + return content; } catch (Exception e) { LOGGER.error("Error while getting server modpack content", e); } finally { @@ -447,52 +425,6 @@ public static Optional refreshServerModpackContent(I return Optional.empty(); } - public static Optional parseStreamToModpack(List rawBytes) { - String response = null; - - // get list of bytes[] to one byte[] object - long len = rawBytes.stream().mapToLong(b -> b.length).sum(); - byte[] bytes = new byte[(int) len]; - int pos = 0; - for (byte[] b : rawBytes) { - System.arraycopy(b, 0, bytes, pos, b.length); - pos += b.length; - } - - try (InputStreamReader isr = new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8)) { - JsonElement element = JsonParser.parseReader(isr); // Needed to parse by deprecated method because of older minecraft versions (<1.17.1) - if (element != null && !element.isJsonArray()) { - JsonObject obj = element.getAsJsonObject(); - response = obj.toString(); - } - } catch (Exception e) { - LOGGER.error("Couldn't parse modpack content", e); - } - - if (response == null) { - LOGGER.error("Couldn't parse modpack content"); - return Optional.empty(); - } - - Jsons.ModpackContentFields serverModpackContent = GSON.fromJson(response, Jsons.ModpackContentFields.class); - - if (serverModpackContent == null) { - LOGGER.error("Couldn't parse modpack content"); - return Optional.empty(); - } - - if (serverModpackContent.list.isEmpty()) { - LOGGER.error("Modpack content is empty!"); - return Optional.empty(); - } - - if (potentiallyMalicious(serverModpackContent)) { - return Optional.empty(); - } - - return Optional.of(serverModpackContent); - } - // check if modpackContent is valid/isn't malicious public static boolean potentiallyMalicious(Jsons.ModpackContentFields serverModpackContent) { String modpackName = serverModpackContent.modpackName; diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 2e82a8a2..141791bf 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -131,7 +131,7 @@ private synchronized void downloadNext() { try { downloadTask(hashAndPath, queuedDownload); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Error while downloading file - {}", queuedDownload.file.getFileName(), e); } }, DOWNLOAD_EXECUTOR); From cb2719062cc8bca7d9f89714212ab34661efcb12 Mon Sep 17 00:00:00 2001 From: skidam Date: Sat, 1 Mar 2025 12:17:52 +0100 Subject: [PATCH 37/50] Dont load content every frame on ChangelogScreen --- .../automodpack/client/ui/ChangelogScreen.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java index 102bcbfc..f0d63e98 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java @@ -99,13 +99,15 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, drawSummaryOfChanges(matrices); } - private void drawSummaryOfChanges(VersionedMatrices matrices) { - - var optionalModpackContentFile = ModpackContentTools.getModpackContentFile(modpackDir); + private Jsons.ModpackContentFields modpackContent = null; - if (optionalModpackContentFile.isEmpty()) return; + private void drawSummaryOfChanges(VersionedMatrices matrices) { - Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(optionalModpackContentFile.get()); + if (modpackContent == null) { + var optionalModpackContentFile = ModpackContentTools.getModpackContentFile(modpackDir); + if (optionalModpackContentFile.isEmpty()) return; + modpackContent = ConfigTools.loadModpackContent(optionalModpackContentFile.get()); + } int modsAdded = 0; int modsRemoved = 0; From bc94b5b3565fa0c2119b9cba68794cf52a5960f8 Mon Sep 17 00:00:00 2001 From: skidam Date: Sat, 1 Mar 2025 12:28:47 +0100 Subject: [PATCH 38/50] reinitialize workarounds --- .../pl/skidam/automodpack_loader_core/client/ModpackUpdater.java | 1 + 1 file changed, 1 insertion(+) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index a7a91fb9..9bfcfad6 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -160,6 +160,7 @@ public void startUpdate() { // Rename modpack modpackDir = ModpackUtils.renameModpackDir(serverModpackContent, modpackDir); modpackContentFile = modpackDir.resolve(modpackContentFile.getFileName()); + workaroundUtil = new WorkaroundUtil(modpackDir); Iterator iterator = serverModpackContent.list.iterator(); From 54048998bc4f8820c02097e628181a1261691adb Mon Sep 17 00:00:00 2001 From: skidam Date: Sat, 1 Mar 2025 14:08:36 +0100 Subject: [PATCH 39/50] Fix encoding issues on windows using `Files.writeString()` --- .../client/ModpackUpdater.java | 13 ++++++------- .../client/ModpackUtils.java | 16 +++++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 9bfcfad6..32fddcf4 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -28,11 +28,10 @@ public class ModpackUpdater { public long totalBytesToDownload = 0; public boolean fullDownload = false; private Jsons.ModpackContentFields serverModpackContent; - private String unModifiedSMC; + private String modpackContentJson; private WorkaroundUtil workaroundUtil; public Map> failedDownloads = new HashMap<>(); private final Set newDownloadedFiles = new HashSet<>(); // Only files which did not exist before. Because some files may have the same name/path and be updated. - private InetSocketAddress modpackAddress; private Secrets.Secret modpackSecret; private Path modpackDir; @@ -64,7 +63,7 @@ public void prepareUpdate(Jsons.ModpackContentFields modpackContent, InetSocketA } // Prepare for modpack update - unModifiedSMC = GSON.toJson(serverModpackContent); + modpackContentJson = GSON.toJson(serverModpackContent); // Create directories if they don't exist if (!Files.exists(modpackDir)) { @@ -79,7 +78,7 @@ public void prepareUpdate(Jsons.ModpackContentFields modpackContent, InetSocketA // Check if an update is needed if (!ModpackUtils.isUpdate(serverModpackContent, modpackDir)) { LOGGER.info("Modpack is up to date"); - Files.write(modpackContentFile, unModifiedSMC.getBytes()); + Files.writeString(modpackContentFile, modpackContentJson); CheckAndLoadModpack(); return; } @@ -315,7 +314,7 @@ public void startUpdate() { // or fail and then show the error var refreshedContent = refreshedContentOptional.get(); - this.unModifiedSMC = GSON.toJson(refreshedContent); + this.modpackContentJson = GSON.toJson(refreshedContent); // filter list to only the failed downloads var refreshedFilteredList = refreshedContent.list.stream().filter(item -> hashesToRefresh.containsKey(item.file)).toList(); @@ -355,10 +354,10 @@ public void startUpdate() { } } - LOGGER.info("Done, saving {}", modpackContentFile.getFileName().toString()); + LOGGER.info("Done, saving {}, Json: {}", modpackContentFile.toAbsolutePath().normalize(), modpackContentJson); // Downloads completed - Files.write(modpackContentFile, unModifiedSMC.getBytes()); + Files.writeString(modpackContentFile, modpackContentJson); Path cwd = Path.of(System.getProperty("user.dir")); CustomFileUtils.deleteDummyFiles(cwd, serverModpackContent.list); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 8f6403b2..c349018c 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -299,13 +299,15 @@ public static boolean selectModpack(Path modpackDirToSelect, InetSocketAddress m final String modpackToSelect = modpackDirToSelect.getFileName().toString(); String selectedModpack = clientConfig.selectedModpack; - // Save current editable files - Path selectedModpackDir = modpacksDir.resolve(selectedModpack); - Path selectedModpackContentFile = selectedModpackDir.resolve(hostModpackContentFile.getFileName()); - Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(selectedModpackContentFile); - if (modpackContent != null) { - Set editableFiles = getEditableFiles(modpackContent.list); - ModpackUtils.preserveEditableFiles(selectedModpackDir, editableFiles, newDownloadedFiles); + if (selectedModpack != null && !selectedModpack.isBlank()) { + // Save current editable files + Path selectedModpackDir = modpacksDir.resolve(selectedModpack); + Path selectedModpackContentFile = selectedModpackDir.resolve(hostModpackContentFile.getFileName()); + Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(selectedModpackContentFile); + if (modpackContent != null) { + Set editableFiles = getEditableFiles(modpackContent.list); + ModpackUtils.preserveEditableFiles(selectedModpackDir, editableFiles, newDownloadedFiles); + } } // Copy editable files from modpack to select From 4e9119e63b9d5f0a623411f9f38f8d8c4e3b5572 Mon Sep 17 00:00:00 2001 From: skidam Date: Sat, 1 Mar 2025 15:59:25 +0100 Subject: [PATCH 40/50] Minor fixes, forge services locator now checks nested jars, refactor code for improved readability and performance --- .../automodpack_core/config/ConfigTools.java | 1 - .../utils/FileInspection.java | 68 +++++++++++-------- .../utils/WorkaroundUtil.java | 4 +- .../automodpack_loader_core/Preload.java | 12 +++- .../client/ModpackUpdater.java | 15 +++- .../client/ModpackUtils.java | 7 +- .../utils/DownloadManager.java | 6 ++ .../automodpack/client/ui/DownloadScreen.java | 4 -- 8 files changed, 70 insertions(+), 47 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java index 9b82deeb..580b13de 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java @@ -99,7 +99,6 @@ public static Jsons.ModpackContentFields loadModpackContent(Path modpackContentF try { if (Files.isRegularFile(modpackContentFile)) { String json = Files.readString(modpackContentFile); - LOGGER.warn("Reading modpack content from file: {} - len: {}, Json: {}", modpackContentFile.toAbsolutePath().normalize(), Files.size(modpackContentFile), json); return GSON.fromJson(json, Jsons.ModpackContentFields.class); } } catch (Exception e) { diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java index 14ce64df..5e4354e0 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/FileInspection.java @@ -21,13 +21,14 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; public class FileInspection { public static boolean isMod(Path file) { - return getModID(file) != null || hasSpecificServices(file) || isFMLMod(file); + return getModID(file) != null || hasSpecificServices(file); } public record Mod(String modID, String hash, Collection providesIDs, String modVersion, Path modPath, LoaderManagerService.EnvironmentType environmentType, Collection dependencies) {} @@ -103,52 +104,59 @@ public static Path getAutoModpackJar() { } } + private static final Set services = Set.of( + "META-INF/services/net.minecraftforge.forgespi.locating.IModLocator", + "META-INF/services/net.minecraftforge.forgespi.locating.IDependencyLocator", + "META-INF/services/net.minecraftforge.forgespi.language.IModLanguageProvider", + "META-INF/services/net.neoforged.neoforgespi.locating.IModLocator", + "META-INF/services/net.neoforged.neoforgespi.locating.IDependencyLocator", + "META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator", + "META-INF/services/net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper" + ); + // Checks for neo/forge mod locators + // TODO: check nested jars recursively if needed public static boolean hasSpecificServices(Path file) { if (!file.getFileName().toString().endsWith(".jar") || !Files.exists(file)) { return false; } - String[] services = { - "META-INF/services/net.minecraftforge.forgespi.locating.IModLocator", - "META-INF/services/net.minecraftforge.forgespi.locating.IDependencyLocator", - "META-INF/services/net.neoforged.neoforgespi.locating.IModLocator", - "META-INF/services/net.neoforged.neoforgespi.locating.IDependencyLocator", - "META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator", - "META-INF/services/net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper" - }; - try (ZipFile zipFile = new ZipFile(file.toFile())) { + // Direct lookup for known service entries for (String service : services) { - if (zipFile.getEntry(service) != null) { + ZipEntry entry = zipFile.getEntry(service); + if (entry != null) { return true; } } - } catch (IOException e) { - e.printStackTrace(); - } - return false; - } - - // Check for /META-INF/MANIFEST.MF, if FMLModType entry exists - public static boolean isFMLMod(Path file) { - if (!file.getFileName().toString().endsWith(".jar") || !Files.exists(file)) { - return false; - } - - try (ZipFile zipFile = new ZipFile(file.toFile())) { - ZipEntry entry = zipFile.getEntry("META-INF/MANIFEST.MF"); - if (entry == null) { + String jarjarPrefix = "META-INF/jarjar/"; + ZipEntry jarjarEntry = zipFile.getEntry(jarjarPrefix); + if (jarjarEntry == null) { return false; } - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(zipFile.getInputStream(entry)))) { - return reader.lines().anyMatch(line -> line.startsWith("FMLModType:")); + // Check nested JARs in META-INF/jarjar/ + for (ZipEntry entry : Collections.list(zipFile.entries())) { + String entryName = entry.getName(); + + if (!entry.isDirectory() && entryName.startsWith(jarjarPrefix) && entryName.endsWith(".jar")) { + try (InputStream inputStream = zipFile.getInputStream(entry); + ZipInputStream zipInputStream = new ZipInputStream(inputStream)) { + + ZipEntry nestedEntry; + while ((nestedEntry = zipInputStream.getNextEntry()) != null) { + if (services.contains(nestedEntry.getName())) { + return true; + } + } + } catch (IOException e) { + LOGGER.error("Error reading nested JAR in {}: {}", file, e.getMessage()); + } + } } } catch (IOException e) { - LOGGER.error("Error reading manifest for {}: {}", file, e.getMessage()); + LOGGER.error("Error examining JAR file {}: {}", file, e.getMessage()); } return false; diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java b/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java index df0f97e1..43856475 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java @@ -8,9 +8,7 @@ import java.util.HashSet; import java.util.Set; -import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; -import static pl.skidam.automodpack_core.GlobalVariables.clientConfig; - +import static pl.skidam.automodpack_core.GlobalVariables.*; public class WorkaroundUtil { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java index c13345c5..082da6c5 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java @@ -169,10 +169,20 @@ private void loadConfigs() { ConfigTools.save(clientConfigFile, clientConfig); } + try { + Files.createDirectories(privateDir); + if (Files.exists(privateDir) && System.getProperty("os.name").toLowerCase().contains("win")) { + Files.setAttribute(privateDir, "dos:hidden", true); + } + } catch (IOException e) { + LOGGER.error("Failed to create private directory", e); + } + + if (serverConfig == null || clientConfig == null) { throw new RuntimeException("Failed to load config!"); } - LOGGER.info("Loaded config! took " + (System.currentTimeMillis() - startTime) + "ms"); + LOGGER.info("Loaded config! took {}ms", System.currentTimeMillis() - startTime); } } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 32fddcf4..2fbb70db 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -21,6 +21,7 @@ import static pl.skidam.automodpack_core.GlobalVariables.*; import static pl.skidam.automodpack_core.config.ConfigTools.GSON; +// TODO: clean up this mess public class ModpackUpdater { public Changelogs changelogs = new Changelogs(); public DownloadManager downloadManager; @@ -280,8 +281,12 @@ public void startUpdate() { LOGGER.info("Finished downloading files in {}ms", System.currentTimeMillis() - startFetching); } - downloadManager.cancelAllAndShutdown(); + if (downloadManager.isCanceled()) { + LOGGER.warn("Download canceled"); + return; + } + downloadManager.cancelAllAndShutdown(); totalBytesToDownload = 0; Map hashesToRefresh = new HashMap<>(); // File name, hash @@ -348,13 +353,19 @@ public void startUpdate() { } downloadManager.joinAll(); + + if (downloadManager.isCanceled()) { + LOGGER.warn("Download canceled"); + return; + } + downloadManager.cancelAllAndShutdown(); LOGGER.info("Finished refreshed downloading files in {}ms", System.currentTimeMillis() - startFetching); } } - LOGGER.info("Done, saving {}, Json: {}", modpackContentFile.toAbsolutePath().normalize(), modpackContentJson); + LOGGER.info("Done, saving {}", modpackContentFile); // Downloads completed Files.writeString(modpackContentFile, modpackContentJson); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index c349018c..3ca87b91 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -405,12 +405,7 @@ private static Optional fetchModpackContent(InetSock var future = operation.apply(client); Path path = future.get(); var content = Optional.ofNullable(ConfigTools.loadModpackContent(path)); - LOGGER.info("{} the modpack content: {} - {}", fetchType, path.toAbsolutePath().normalize(), content); - if (path.toAbsolutePath().normalize().equals(modpackContentTempFile.toAbsolutePath().normalize())) { - if (Files.deleteIfExists(path)) { - LOGGER.info("Deleted temporary modpack content file: {}", path); - } - } + Files.deleteIfExists(modpackContentTempFile); if (content.isPresent() && potentiallyMalicious(content.get())) { return Optional.empty(); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java index 141791bf..7bf4b1b9 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java @@ -22,6 +22,7 @@ public class DownloadManager { private static final int BUFFER_SIZE = 128 * 1024; private final ExecutorService DOWNLOAD_EXECUTOR = Executors.newFixedThreadPool(MAX_DOWNLOADS_IN_PROGRESS, new CustomThreadFactoryBuilder().setNameFormat("AutoModpackDownload-%d").build()); private DownloadClient downloadClient = null; + private boolean cancelled = false; private final Map queuedDownloads = new ConcurrentHashMap<>(); public final Map downloadsInProgress = new ConcurrentHashMap<>(); private long bytesDownloaded = 0; @@ -242,7 +243,12 @@ public boolean isRunning() { return !DOWNLOAD_EXECUTOR.isShutdown(); } + public boolean isCanceled() { + return cancelled; + } + public void cancelAllAndShutdown() { + cancelled = true; queuedDownloads.clear(); downloadsInProgress.forEach((url, downloadData) -> { downloadData.future.cancel(true); diff --git a/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java index 3f5f483d..33062112 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java @@ -15,8 +15,6 @@ import pl.skidam.automodpack.client.ui.versioned.VersionedText; import pl.skidam.automodpack_loader_core.utils.SpeedMeter; -import static pl.skidam.automodpack_core.GlobalVariables.LOGGER; - public class DownloadScreen extends VersionedScreen { private static final Identifier PROGRESS_BAR_EMPTY_TEXTURE = Common.id("gui/progress-bar-empty.png"); @@ -209,8 +207,6 @@ public void cancelDownload() { downloadManager.cancelAllAndShutdown(); } - LOGGER.warn("Download canceled"); - // TODO delete files that were downloaded // we will use the same method as to modpacks manager From 5572c7949e15fa9d049ca0c3285bee892094b3f6 Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 2 Mar 2025 12:59:02 +0100 Subject: [PATCH 41/50] logs ref: 334 --- .../skidam/automodpack_core/protocol/netty/NettyServer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 0b8ac7a6..4648abac 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -109,12 +109,14 @@ public Optional start() { int port = serverConfig.hostPort; InetAddress address = new InetSocketAddress(port).getAddress(); + LOGGER.info("Starting modpack host server on {}:{}", address, port); Class socketChannelClass; MultithreadEventLoopGroup eventLoopGroup; if (Epoll.isAvailable()) { socketChannelClass = EpollServerSocketChannel.class; eventLoopGroup = new EpollEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Epoll Server IO #%d").setDaemon(true).build()); + LOGGER.info("epoll is available"); } else { socketChannelClass = NioServerSocketChannel.class; eventLoopGroup = new NioEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Server IO #%d").setDaemon(true).build()); @@ -135,6 +137,7 @@ protected void initChannel(SocketChannel ch) throws Exception { .syncUninterruptibly(); } catch (Exception e) { LOGGER.error("Failed to start Netty server", e); + e.printStackTrace(); return Optional.empty(); } @@ -225,7 +228,6 @@ private boolean canStart() { } shouldHost = true; - LOGGER.info("Starting modpack host server on port {}", serverConfig.hostPort); return true; } } From 7e069aad3f0b127453a8b403489465c1a8418a9d Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 2 Mar 2025 13:02:37 +0100 Subject: [PATCH 42/50] bind on `hostLocalIp` ref: #334 --- .../automodpack_core/protocol/netty/NettyServer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 4648abac..312334af 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -19,7 +19,6 @@ import pl.skidam.automodpack_core.utils.AddressHelpers; import pl.skidam.automodpack_core.utils.ObservableMap; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; @@ -108,8 +107,9 @@ public Optional start() { } int port = serverConfig.hostPort; - InetAddress address = new InetSocketAddress(port).getAddress(); - LOGGER.info("Starting modpack host server on {}:{}", address, port); + String localIp = serverConfig.hostLocalIp; + InetSocketAddress bindAddress = new InetSocketAddress(localIp, port); + LOGGER.info("Starting modpack host server on {}:{}", bindAddress, port); Class socketChannelClass; MultithreadEventLoopGroup eventLoopGroup; @@ -132,7 +132,7 @@ protected void initChannel(SocketChannel ch) throws Exception { } }) .group(eventLoopGroup) - .localAddress(address, port) + .localAddress(bindAddress) .bind() .syncUninterruptibly(); } catch (Exception e) { From aa8c960761d3beaf7285f9e8b8d2d097049b1026 Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 2 Mar 2025 13:23:41 +0100 Subject: [PATCH 43/50] always bind on 0.0.0.0 ref: #334 --- .../automodpack_core/protocol/netty/NettyServer.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index 312334af..09aa703a 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -107,16 +107,14 @@ public Optional start() { } int port = serverConfig.hostPort; - String localIp = serverConfig.hostLocalIp; - InetSocketAddress bindAddress = new InetSocketAddress(localIp, port); - LOGGER.info("Starting modpack host server on {}:{}", bindAddress, port); + InetSocketAddress bindAddress = new InetSocketAddress("0.0.0.0", port); + LOGGER.info("Starting modpack host server on {}", bindAddress); Class socketChannelClass; MultithreadEventLoopGroup eventLoopGroup; if (Epoll.isAvailable()) { socketChannelClass = EpollServerSocketChannel.class; eventLoopGroup = new EpollEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Epoll Server IO #%d").setDaemon(true).build()); - LOGGER.info("epoll is available"); } else { socketChannelClass = NioServerSocketChannel.class; eventLoopGroup = new NioEventLoopGroup(new CustomThreadFactoryBuilder().setNameFormat("AutoModpack Server IO #%d").setDaemon(true).build()); @@ -137,7 +135,6 @@ protected void initChannel(SocketChannel ch) throws Exception { .syncUninterruptibly(); } catch (Exception e) { LOGGER.error("Failed to start Netty server", e); - e.printStackTrace(); return Optional.empty(); } From 0fefa5c3c3b32e19485bff5407fba4c217b31caa Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 2 Mar 2025 18:53:44 +0100 Subject: [PATCH 44/50] Fixes to selfupdater code ref: #333 --- .../automodpack_core/auth/SecretsStore.java | 4 +- .../automodpack_loader_core/Preload.java | 25 ++++++++--- .../automodpack_loader_core/ReLauncher.java | 43 ++++++++++++------- .../automodpack_loader_core/SelfUpdater.java | 6 +-- .../client/ModpackUpdater.java | 6 ++- .../networking/packet/DataC2SPacket.java | 9 ++++ 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java index 115ad091..fbe47bc8 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java +++ b/core/src/main/java/pl/skidam/automodpack_core/auth/SecretsStore.java @@ -39,7 +39,7 @@ public Secrets.Secret get(String key) { return cache.get(key); } - public void save(String key, Secrets.Secret secret) { + public void save(String key, Secrets.Secret secret) throws IllegalArgumentException { if (key == null || key.isBlank() || secret == null || secret.secret().isBlank()) throw new IllegalArgumentException("Key or secret cannot be null or blank"); load(); @@ -75,7 +75,7 @@ public static Secrets.Secret getClientSecret(String modpack) { return clientSecrets.get(modpack); } - public static void saveClientSecret(String modpack, Secrets.Secret secret) { + public static void saveClientSecret(String modpack, Secrets.Secret secret) throws IllegalArgumentException { clientSecrets.save(modpack, secret); } } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java index 082da6c5..bfbc79bd 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java @@ -58,6 +58,21 @@ private void updateAll() { return; } + // Check if link is old http link, and parse it to new format (beta 24 -> beta 25) + if (selectedModpackLink.startsWith("http") && selectedModpackLink.contains("/automodpack")) { + var newSelectedModpackLink = selectedModpackLink; + newSelectedModpackLink = newSelectedModpackLink.replace("http://", ""); + newSelectedModpackLink = newSelectedModpackLink.replace("https://", ""); + String[] split = newSelectedModpackLink.split("/automodpack"); + newSelectedModpackLink = split[0]; + if (newSelectedModpackLink != null && !newSelectedModpackLink.isBlank()) { + LOGGER.info("Updated modpack link to new format: {} -> {}", selectedModpackLink, newSelectedModpackLink); + clientConfig.installedModpacks.put(clientConfig.selectedModpack, newSelectedModpackLink); + ConfigTools.save(clientConfigFile, clientConfig); + selectedModpackLink = newSelectedModpackLink; + } + } + InetSocketAddress selectedModpackAddress = AddressHelpers.parse(selectedModpackLink); Secrets.Secret secret = SecretsStore.getClientSecret(clientConfig.selectedModpack); @@ -67,16 +82,16 @@ private void updateAll() { // Use the latest modpack content if available if (optionalLatestModpackContent.isPresent()) { latestModpackContent = optionalLatestModpackContent.get(); + + // Update AutoModpack to server version only if we can get newest modpack content + if (SelfUpdater.update(latestModpackContent)) { + return; + } } // Delete dummy files CustomFileUtils.deleteDummyFiles(Path.of(System.getProperty("user.dir")), latestModpackContent == null ? null : latestModpackContent.list); - // Update AutoModpack - if (SelfUpdater.update(latestModpackContent)) { - return; - } - // Update modpack new ModpackUpdater().prepareUpdate(latestModpackContent, selectedModpackAddress, secret, selectedModpackDir); } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java index 91ef3da0..754dde92 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java @@ -2,7 +2,6 @@ import pl.skidam.automodpack_core.callbacks.Callback; import pl.skidam.automodpack_loader_core.client.Changelogs; -import pl.skidam.automodpack_loader_core.loader.LoaderManager; import pl.skidam.automodpack_core.loader.LoaderManagerService; import pl.skidam.automodpack_loader_core.screen.ScreenManager; import pl.skidam.automodpack_loader_core.utils.UpdateType; @@ -46,28 +45,42 @@ public ReLauncher(Path modpackDir, UpdateType updateType, Changelogs changelogs) } public void restart(boolean restartInPreload, Callback... callbacks) { - if (preload && !restartInPreload) return; + if (preload && !restartInPreload) { + runCallbacks(callbacks); + return; + } boolean isClient = LOADER_MANAGER.getEnvironmentType() == LoaderManagerService.EnvironmentType.CLIENT; boolean isHeadless = GraphicsEnvironment.isHeadless(); if (isClient) { - if (updateType != null && new ScreenManager().getScreenString().isPresent() && !new ScreenManager().getScreenString().get().toLowerCase().contains("restartscreen")) { - new ScreenManager().restart(modpackDir, updateType, changelogs); - return; - } + handleClientRestart(callbacks, isHeadless); + } else { + handleServerRestart(callbacks); + } + } - if (preload) { - if (isHeadless) { - LOGGER.info("Please restart the game to apply updates!"); - } else { - new Windows().restartWindow(updateMessage, callbacks); - } + private void handleClientRestart(Callback[] callbacks, boolean isHeadless) { + if (updateType != null && new ScreenManager().getScreenString().isPresent()) { + new ScreenManager().restart(modpackDir, updateType, changelogs); + } else if (preload) { + if (isHeadless) { + LOGGER.info("Please restart the game to apply updates!"); + } else { + new Windows().restartWindow(updateMessage, callbacks); } - } else { - LOGGER.info("Please restart the server to apply updates!"); } + runCallbacks(callbacks); + } + + private void handleServerRestart(Callback[] callbacks) { + LOGGER.info("Please restart the server to apply updates!"); + runCallbacks(callbacks); + System.exit(0); + } + + private void runCallbacks(Callback[] callbacks) { for (Callback callback : callbacks) { try { callback.run(); @@ -75,7 +88,5 @@ public void restart(boolean restartInPreload, Callback... callbacks) { e.printStackTrace(); } } - - System.exit(0); } } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java index be250544..12440eee 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java @@ -3,7 +3,6 @@ import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.utils.CustomFileUtils; import pl.skidam.automodpack_core.loader.LoaderManagerService; -import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.platforms.ModrinthAPI; import pl.skidam.automodpack_loader_core.screen.ScreenManager; import pl.skidam.automodpack_loader_core.utils.DownloadManager; @@ -168,13 +167,12 @@ public static void installModVersion(ModrinthAPI automodpack) { newAutomodpackJar = AUTOMODPACK_JAR.getParent().resolve(automodpackUpdateJar.getFileName()); - // preload classes - new ReLauncher(); var updateType = UpdateType.AUTOMODPACK; + var relauncher = new ReLauncher(updateType); CustomFileUtils.copyFile(automodpackUpdateJar, newAutomodpackJar); CustomFileUtils.forceDelete(automodpackUpdateJar); - new ReLauncher(updateType).restart(true, () -> { + relauncher.restart(true, () -> { CustomFileUtils.forceDelete(AUTOMODPACK_JAR); LOGGER.info("Successfully updated AutoModpack!"); }); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 2fbb70db..0a8ca3aa 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -412,7 +412,11 @@ public void startUpdate() { // returns true if restart is required private boolean applyModpack() throws Exception { ModpackUtils.selectModpack(modpackDir, modpackAddress, newDownloadedFiles); - SecretsStore.saveClientSecret(clientConfig.selectedModpack, modpackSecret); + try { // try catch this error there because we don't want to stop the whole method just because of that + SecretsStore.saveClientSecret(clientConfig.selectedModpack, modpackSecret); + } catch (IllegalArgumentException e) { + LOGGER.error("Failed to save client secret", e); + } Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(modpackContentFile); if (modpackContent == null) { diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index f713bfaf..7135b680 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -15,11 +15,13 @@ import pl.skidam.automodpack_loader_core.utils.UpdateType; import java.net.InetSocketAddress; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; import java.util.concurrent.CompletableFuture; import static pl.skidam.automodpack_core.GlobalVariables.*; +import static pl.skidam.automodpack_core.config.ConfigTools.GSON; public class DataC2SPacket { public static CompletableFuture receive(MinecraftClient minecraftClient, ClientLoginNetworkHandler handler, PacketByteBuf buf) { @@ -68,6 +70,13 @@ public static CompletableFuture receive(MinecraftClient minecraft needsDisconnecting = true; } else { boolean selectedModpackChanged = ModpackUtils.selectModpack(modpackDir, address, Set.of()); + + // save latest modpack content + var modpackContentFile = modpackDir.resolve(hostModpackContentFile.getFileName()); + if (Files.exists(modpackContentFile)) { + Files.writeString(modpackContentFile, GSON.toJson(optionalServerModpackContent.get())); + } + if (selectedModpackChanged) { SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); disconnectImmediately(handler); From cb279d25f7322aae99d65ff92dc70906d5314bdd Mon Sep 17 00:00:00 2001 From: skidam Date: Sun, 2 Mar 2025 20:30:13 +0100 Subject: [PATCH 45/50] bump to beta 26 and deps --- gradle.properties | 2 +- versions/1.21.1-fabric/gradle.properties | 2 +- versions/1.21.4-fabric/gradle.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index e039b74f..fdf4d84c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,6 @@ mixin_extras = 0.3.6 mod_id = automodpack mod_name = AutoModpack -mod_version = 4.0.0-beta25 +mod_version = 4.0.0-beta26 mod_group = pl.skidam.automodpack mod_description = Enjoy a seamless modpack installation process and effortless updates with a user-friendly solution that simplifies management, making your gaming experience a breeze. diff --git a/versions/1.21.1-fabric/gradle.properties b/versions/1.21.1-fabric/gradle.properties index f3dc7760..67961f52 100644 --- a/versions/1.21.1-fabric/gradle.properties +++ b/versions/1.21.1-fabric/gradle.properties @@ -2,7 +2,7 @@ minecraft_version=1.21.1 yarn_mappings=1.21.1+build.3 loom.platform=fabric minecraft_dependency=>=1.21 <=1.21.1 -fabric_version=0.115.0+1.21.1 +fabric_version=0.115.1+1.21.1 # The target mc versions for the mod during mod publishing, separated with \n game_versions=1.21\n1.21.1 \ No newline at end of file diff --git a/versions/1.21.4-fabric/gradle.properties b/versions/1.21.4-fabric/gradle.properties index e368edc1..ac1cddd1 100644 --- a/versions/1.21.4-fabric/gradle.properties +++ b/versions/1.21.4-fabric/gradle.properties @@ -2,7 +2,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loom.platform=fabric minecraft_dependency=>=1.21.4 -fabric_version=0.117.0+1.21.4 +fabric_version=0.118.0+1.21.4 # The target mc versions for the mod during mod publishing, separated with \n game_versions=1.21.4 \ No newline at end of file From 2e0ad546f54d4d15d26377dfe6f98623c26ad357 Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 3 Mar 2025 17:45:31 +0100 Subject: [PATCH 46/50] Generate offline uuids ref: #335 --- .../networking/packet/HandshakeS2CPacket.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index 9b3cb472..df16ff02 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -20,6 +20,9 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.utils.AddressHelpers; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + import static pl.skidam.automodpack.networking.ModPackets.DATA; import static pl.skidam.automodpack_core.GlobalVariables.*; @@ -31,8 +34,18 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han GameProfile profile = ((ServerLoginNetworkHandlerAccessor) handler).getGameProfile(); String playerName = profile.getName(); - if (profile.getId() == null){ - LOGGER.error("Player {} doesn't have UUID: {}", playerName, profile.getId()); + if (playerName == null) { + throw new IllegalStateException("Player name is null"); + } + + if (profile.getId() == null) { + if (server.isOnlineMode()) { + throw new IllegalStateException("Player: " + playerName + " doesn't have UUID"); + } + + // Generate profile with offline uuid + UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(StandardCharsets.UTF_8)); + profile = new GameProfile(offlineUUID, playerName); } if (!connection.isEncrypted()) { @@ -53,7 +66,8 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han } } else { Common.players.put(playerName, true); - loginSynchronizer.waitFor(server.submit(() -> handleHandshake(connection, profile, server.getServerPort(), buf, sender))); + GameProfile finalProfile = profile; + loginSynchronizer.waitFor(server.submit(() -> handleHandshake(connection, finalProfile, server.getServerPort(), buf, sender))); } } From a435855c598d8394fafd23a8a5a2be9d29f7cc95 Mon Sep 17 00:00:00 2001 From: skidam Date: Mon, 3 Mar 2025 17:55:47 +0100 Subject: [PATCH 47/50] bump to beta 27 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fdf4d84c..4b03e2b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,6 @@ mixin_extras = 0.3.6 mod_id = automodpack mod_name = AutoModpack -mod_version = 4.0.0-beta26 +mod_version = 4.0.0-beta27 mod_group = pl.skidam.automodpack mod_description = Enjoy a seamless modpack installation process and effortless updates with a user-friendly solution that simplifies management, making your gaming experience a breeze. From 60ad8e0a551b457afbb259903d36aacbca3079f7 Mon Sep 17 00:00:00 2001 From: skidam Date: Thu, 6 Mar 2025 16:32:03 +0100 Subject: [PATCH 48/50] Fix restart button #337 --- .../java/pl/skidam/automodpack/client/ui/RestartScreen.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java index d6b0efc6..a569654e 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java @@ -2,7 +2,6 @@ import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.util.Formatting; -import pl.skidam.automodpack_loader_core.ReLauncher; import pl.skidam.automodpack.client.audio.AudioManager; import java.nio.file.Path; @@ -55,7 +54,7 @@ public void initWidgets() { }); restartButton = buttonWidget(this.width / 2 + 5, this.height / 2 + 50, 150, 20, VersionedText.translatable("automodpack.restart.confirm").formatted(Formatting.BOLD), button -> { - new ReLauncher().restart(false); + System.exit(0); }); changelogsButton = buttonWidget(this.width / 2 - 75, this.height / 2 + 75, 150, 20, VersionedText.translatable("automodpack.changelog.view"), button -> { From e0b4aa69d5b2817adbb6577a7eb2129bc9d65a25 Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 11 Mar 2025 12:35:58 +0100 Subject: [PATCH 49/50] remove unused code --- .../java/pl/skidam/automodpack_loader_core/ReLauncher.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java index 754dde92..78eb64cf 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/ReLauncher.java @@ -13,19 +13,12 @@ public class ReLauncher { - // TODO clean up this class private static final String updateMessage = "Successfully updated AutoModpack!"; private final Path modpackDir; private final UpdateType updateType; private final Changelogs changelogs; - public ReLauncher() { - modpackDir = null; - updateType = null; - changelogs = null; - } - public ReLauncher(UpdateType updateType) { this.modpackDir = null; this.updateType = updateType; From d97a39f3bacf9613a84b0f8399c5f9eeeaf237ab Mon Sep 17 00:00:00 2001 From: skidam Date: Tue, 11 Mar 2025 12:36:49 +0100 Subject: [PATCH 50/50] Allow offline players on online mode server #339 --- .../automodpack/networking/packet/HandshakeS2CPacket.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java index df16ff02..bf57515c 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/HandshakeS2CPacket.java @@ -39,9 +39,9 @@ public static void receive(MinecraftServer server, ServerLoginNetworkHandler han } if (profile.getId() == null) { - if (server.isOnlineMode()) { - throw new IllegalStateException("Player: " + playerName + " doesn't have UUID"); - } +// if (server.isOnlineMode()) { This may happen with mods like 'easyauth', its possible to have an offline mode player join an online server +// throw new IllegalStateException("Player: " + playerName + " doesn't have UUID"); +// } // Generate profile with offline uuid UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(StandardCharsets.UTF_8));