diff --git a/build.gradle b/build.gradle index 0bc12aa..ef6b1eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,229 +1,108 @@ -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import net.darkhax.curseforgegradle.TaskPublishCurseForge - -import java.nio.file.Files - -plugins { - id 'java-library' - id 'maven-publish' - id 'net.neoforged.moddev' version '2.0.124' - id "com.modrinth.minotaur" version "2.8.10" - id 'net.darkhax.curseforgegradle' version '1.1.26' - id 'idea' +buildscript { + repositories { + maven { url = 'https://maven.minecraftforge.net' } + maven { url = 'https://files.minecraftforge.net/maven' } + jcenter() + maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } + mavenCentral() + } + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT' + classpath 'org.spongepowered:mixingradle:0.6-SNAPSHOT' + } } -tasks.named('wrapper', Wrapper).configure { - // Define wrapper values here so as to not have to always do so when updating gradlew.properties. - // Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with - // documentation attached on cursor hover of gradle classes and methods. However, this comes with increased - // file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards. - // (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`) - distributionType = Wrapper.DistributionType.BIN -} +apply plugin: 'net.minecraftforge.gradle.forge' +apply plugin: 'org.spongepowered.mixin' +apply plugin: 'java' -version = "${rootProject.mod_version}+${rootProject.mod_version_type}.${System.getenv("GITHUB_RUN_NUMBER") == null ? "dev" : System.getenv("GITHUB_RUN_NUMBER")}" group = mod_group_id +version = mod_version +archivesBaseName = mod_id -repositories {} +sourceCompatibility = targetCompatibility = '1.8' -base { - archivesName = mod_id +repositories { + maven { url = 'https://maven.minecraftforge.net' } + maven { url = 'https://files.minecraftforge.net/maven' } + jcenter() + maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } + mavenCentral() } -java.toolchain.languageVersion = JavaLanguageVersion.of(21) - -neoForge { - // Specify the version of NeoForge to use. - version = project.neo_version - - parchment { - mappingsVersion = project.parchment_mappings_version - minecraftVersion = project.parchment_minecraft_version - } - - // This line is optional. Access Transformers are automatically detected - // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') - - // Default run configurations. - // These can be tweaked, removed, or duplicated as needed. - runs { - client { - client() - - // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - server { - server() - programArgument '--nogui' - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - // This run config launches GameTestServer and runs all registered gametests, then exits. - // By default, the server will crash when no gametests are provided. - // The gametest system is also enabled by default for other run configs under the /test command. - gameTestServer { - type = "gameTestServer" - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - data { - clientData() - - // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it - // gameDirectory = project.file('run-data') - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. - programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() - } - - // applies to all the run configs above - configureEach { - // Recommended logging data for a userdev environment - // The markers can be added/remove as needed separated by commas. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. - systemProperty 'forge.logging.markers', 'REGISTRIES' - - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels - logLevel = org.slf4j.event.Level.DEBUG - } - } - - mods { - // define mod <-> source bindings - // these are used to tell the game which sources are for which mod - // multi mod projects should define one per mod - "${mod_id}" { - sourceSet(sourceSets.main) - } - } +dependencies { + compile 'org.spongepowered:mixin:0.8.2' + testCompile 'junit:junit:4.13.2' } -// Include resources generated by data generators. -sourceSets.main.resources { srcDir 'src/generated/resources' } - -// Sets up a dependency configuration called 'localRuntime'. -// This configuration should be used instead of 'runtimeOnly' to declare -// a dependency that will be present for runtime testing but that is -// "optional", meaning it will not be pulled by dependents of this mod. -configurations { - runtimeClasspath.extendsFrom localRuntime -} - -dependencies {} - -// This block of code expands all declared replace properties in the specified resource targets. -// A missing property will result in an error. Properties are expanded using ${} Groovy notation. -var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { - var replaceProperties = [ - minecraft_version : minecraft_version, - minecraft_version_range: minecraft_version_range, - neo_version : neo_version, - mod_id : mod_id, - mod_name : mod_name, - mod_license : mod_license, - mod_version : mod_version, - mod_authors : mod_authors, - mod_description : mod_description +minecraft { + version = "${minecraft_version}-${forge_version}" + runDir = 'run' + mappings = mcp_mappings + clientJvmArgs += [ + '-Dfml.coreMods.load=dev.httxrafa.modflared.mixin.ModflaredMixinLoader', + '-Dmixin.env.disableRefMap=true' ] - inputs.properties replaceProperties - expand replaceProperties - from "src/main/templates" - into "build/generated/sources/modMetadata" -} -// Include the output of "generateModMetadata" as an input directory for the build -// this works with both building through Gradle and the IDE. -sourceSets.main.resources.srcDir generateModMetadata -// To avoid having to run "generateModMetadata" manually, make it run on every project reload -neoForge.ideSyncTask generateModMetadata - -// Example configuration to allow publishing using the maven-publish plugin -publishing { - publications { - register('mavenJava', MavenPublication) { - from components.java - } - } - repositories { - maven { - url "file://${project.projectDir}/repo" - } - } } -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation +mixin { + defaultObfuscationEnv searge } -// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. -idea { - module { - downloadSources = true - downloadJavadoc = true +processResources { + inputs.property 'version', project.version + inputs.property 'mcversion', minecraft_version + filesMatching('mcmod.info') { + expand([ + version: project.version, + mcversion: minecraft_version, + mod_id: mod_id, + mod_name: mod_name, + mod_description: mod_description, + mod_authors: mod_authors, + mod_license: mod_license + ]) } } -// Configure the modrinth publication -modrinth { - token = System.getenv("MODRINTH_TOKEN") - projectId = "${modrinth_project_id}" - versionNumber = "${version}+${minecraft_version}" - versionName = "${version} for ${minecraft_version}" - versionType = "${mod_version_type}" - changelog = generateChangelog() - uploadFile = jar - gameVersions = ["${minecraft_version}"] - - syncBodyFrom = rootProject.file("README.md").text; +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation'] } -tasks.modrinth.dependsOn(tasks.modrinthSyncBody) - -// Configure the curseforge publication -tasks.register('publishCurseForge', TaskPublishCurseForge) { - apiToken = System.getenv("CURSEFORGE_TOKEN") - - // The main file to upload - def mainFile = upload(curseforge_project_id, jar) - mainFile.displayName = "${version} for ${minecraft_version}" - mainFile.releaseType = mod_version_type - mainFile.changelog = generateChangelog() - mainFile.changelogType = 'markdown' - mainFile.addGameVersion(minecraft_version) - mainFile.addModLoader("neoforge") +jar { + manifest { + attributes( + 'FMLCorePluginContainsFMLMod': 'true', + 'FMLCorePlugin': 'dev.httxrafa.modflared.mixin.ModflaredMixinLoader', + 'MixinConfigs': 'modflared.mixins.json' + ) + } + rename 'mixin.refmap.json', 'modflared.refmap.json' } -// Credits: https://github.com/RelativityMC/VMP-fabric -static String generateChangelog() { - final String path = System.getenv("GITHUB_EVENT_RAW_PATH"); - if (path == null || path.isBlank()) return "No changelog was specified. "; - final JsonObject jsonObject = new Gson().fromJson(Files.readString(java.nio.file.Path.of(path)), JsonObject.class); +tasks.extractAnnotationsJar.enabled = false - StringBuilder builder = new StringBuilder(); - builder.append("This version is uploaded automatically by GitHub Actions. \n\n") - .append("Changelog: \n"); - final JsonArray commits = jsonObject.getAsJsonArray("commits"); - if (commits.isEmpty()) { - builder.append("No changes detected. \n"); - } else { - for (JsonElement commit : commits) { - JsonObject object = commit.getAsJsonObject(); - builder.append("- "); - builder.append('[').append(object.get("id").getAsString(), 0, 8).append(']').append('(').append(object.get("url").getAsString()).append(')'); - builder.append(' '); - builder.append(object.get("message").getAsString().split("\n")[0]); - builder.append(" - "); - builder.append(object.get("author").getAsJsonObject().get("name").getAsString()); - builder.append(" ").append('\n'); - } +task shadowJar(type: Jar) { + classifier = 'all' + from zipTree(jar.archivePath) + from { + configurations.compile.filter { dep -> + dep.name.contains('mixin') + }.collect { it.isDirectory() ? it : zipTree(it) } } - return builder.toString(); + exclude 'META-INF/*.SF' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + manifest { + attributes( + 'FMLCorePluginContainsFMLMod': 'true', + 'FMLCorePlugin': 'dev.httxrafa.modflared.mixin.ModflaredMixinLoader', + 'MixinConfigs': 'modflared.mixins.json' + ) + } + rename 'mixin.refmap.json', 'modflared.refmap.json' } + +shadowJar.dependsOn reobfJar +build.dependsOn shadowJar diff --git a/gradle.properties b/gradle.properties index 8eed26c..d6efc3a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,34 +1,14 @@ -# Sets default memory used for gradle commands. Can be overridden by user or command line properties. org.gradle.jvmargs=-Xmx2G org.gradle.daemon=true -org.gradle.parallel=true -org.gradle.caching=true -org.gradle.configuration-cache=true -parchment_minecraft_version=1.21.10 -parchment_mappings_version=2025.10.12 - -## Environment Properties - -minecraft_version=1.21.11 -minecraft_version_range=[1.21.11] -neo_version=21.11.6-beta - -## Mod Properties +minecraft_version=1.12.2 +forge_version=14.23.5.2847 +mcp_mappings=stable_39 mod_id=modflared mod_name=Modflared mod_license=MIT -mod_version=1.6.0 +mod_version=1.12.2-legacy.1 mod_group_id=dev.httxrafa.modflared -mod_authors=HttpRafa, Contributers +mod_authors=HttpRafa, Contributors mod_description=Automatically connects you to a Cloudflare tunnel without having to install cloudflared separately. - -## Modrinth Properties -modrinth_project_id=modflared - -## Curseforge Properties -curseforge_project_id=997330 - -## Publish Properties -mod_version_type=release \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dbc3ce4..1a57a08 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 38cd19e..750bc11 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,2 @@ -pluginManagement { - repositories { - gradlePluginPortal() - } -} - -plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' -} \ No newline at end of file +// ForgeGradle 1.12.2 single-project settings +rootProject.name = 'modflared' diff --git a/src/main/java/dev/httxrafa/modflared/Modflared.java b/src/main/java/dev/httxrafa/modflared/Modflared.java index afa01d3..fce3603 100644 --- a/src/main/java/dev/httxrafa/modflared/Modflared.java +++ b/src/main/java/dev/httxrafa/modflared/Modflared.java @@ -2,40 +2,52 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.mojang.logging.LogUtils; import dev.httxrafa.modflared.tunnel.manager.TunnelManager; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.fml.common.Mod; -import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; -import org.slf4j.Logger; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -@Mod(value = Modflared.MOD_ID, dist = Dist.CLIENT) -@EventBusSubscriber(modid = Modflared.MOD_ID, value = Dist.CLIENT) +@Mod( + modid = Modflared.MOD_ID, + name = Modflared.MOD_NAME, + version = Modflared.VERSION, + clientSideOnly = true +) public class Modflared { - public static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); - public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().serializeNulls().create(); - public static final String MOD_ID = "modflared"; - public static final Logger LOGGER = LogUtils.getLogger(); + public static final String MOD_NAME = "Modflared"; + public static final String VERSION = "1.12.2-legacy.1"; + + public static final Logger LOGGER = LogManager.getLogger(MOD_NAME); + public static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); + public static final Gson GSON = new GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .serializeNulls() + .create(); public static final TunnelManager TUNNEL_MANAGER = new TunnelManager(); - @SubscribeEvent - static void onClientSetup(FMLClientSetupEvent event) { + @EventHandler + public void init(FMLInitializationEvent event) { TUNNEL_MANAGER.initDirectories(); TUNNEL_MANAGER.prepareBinary(); TUNNEL_MANAGER.loadForcedTunnels(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - TUNNEL_MANAGER.closeTunnels(); - EXECUTOR.shutdownNow(); - })); - } + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + TUNNEL_MANAGER.closeTunnels(); + EXECUTOR.shutdownNow(); + } + }, "Modflared Shutdown")); + LOGGER.info("Modflared client setup complete"); + } } diff --git a/src/main/java/dev/httxrafa/modflared/binary/Cloudflared.java b/src/main/java/dev/httxrafa/modflared/binary/Cloudflared.java index 1d794f1..f1cf998 100644 --- a/src/main/java/dev/httxrafa/modflared/binary/Cloudflared.java +++ b/src/main/java/dev/httxrafa/modflared/binary/Cloudflared.java @@ -20,9 +20,10 @@ public Cloudflared(String version) { public abstract String[] buildCommand(RunningTunnel.Access access); public static CompletableFuture create() { - var local = LocalCloudflared.tryCreate(); - if(local != null) return CompletableFuture.completedFuture(local); - + Cloudflared local = LocalCloudflared.tryCreate(); + if(local != null) { + return CompletableFuture.completedFuture(local); + } return DownloadedCloudflared.tryCreate(); } diff --git a/src/main/java/dev/httxrafa/modflared/binary/download/CloudflaredDownload.java b/src/main/java/dev/httxrafa/modflared/binary/download/CloudflaredDownload.java index 64caee5..59d4adc 100644 --- a/src/main/java/dev/httxrafa/modflared/binary/download/CloudflaredDownload.java +++ b/src/main/java/dev/httxrafa/modflared/binary/download/CloudflaredDownload.java @@ -1,9 +1,5 @@ package dev.httxrafa.modflared.binary.download; -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; - public enum CloudflaredDownload { WINDOW_32("windows", "x86", "cloudflared-windows-386.exe", "cloudflared-windows-386.exe"), @@ -27,15 +23,19 @@ public enum CloudflaredDownload { this.downloadFile = downloadFile; } - public static @NotNull CloudflaredDownload find() { - String osName = System.getProperty("os.name").toLowerCase(); - String arch = System.getProperty("os.arch").toLowerCase(); - var download = Arrays.stream(CloudflaredDownload.values()).filter(item -> osName.contains(item.osName) && arch.contains(item.arch)).findFirst(); - if(download.isPresent()) { - return download.get(); - } else { - throw new IllegalStateException("Cloudflared could not be downloaded because no binary file was found for the current operating system"); + public static CloudflaredDownload find() { + return find(System.getProperty("os.name"), System.getProperty("os.arch")); + } + + public static CloudflaredDownload find(String osNameValue, String archValue) { + String osName = osNameValue.toLowerCase(); + String arch = archValue.toLowerCase(); + for (CloudflaredDownload item : CloudflaredDownload.values()) { + if (osName.contains(item.osName) && arch.contains(item.arch)) { + return item; + } } + throw new IllegalStateException("Cloudflared could not be downloaded because no binary file was found for the current operating system"); } public String osName() { diff --git a/src/main/java/dev/httxrafa/modflared/binary/download/DownloadedCloudflared.java b/src/main/java/dev/httxrafa/modflared/binary/download/DownloadedCloudflared.java index 55400db..cb0ef15 100644 --- a/src/main/java/dev/httxrafa/modflared/binary/download/DownloadedCloudflared.java +++ b/src/main/java/dev/httxrafa/modflared/binary/download/DownloadedCloudflared.java @@ -1,14 +1,10 @@ package dev.httxrafa.modflared.binary.download; -import com.mojang.datafixers.util.Pair; import dev.httxrafa.modflared.Modflared; import dev.httxrafa.modflared.binary.Cloudflared; import dev.httxrafa.modflared.github.GithubAPI; import dev.httxrafa.modflared.tunnel.RunningTunnel; import dev.httxrafa.modflared.tunnel.manager.TunnelManager; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.lwjgl.system.Platform; import java.io.*; import java.net.URI; @@ -18,6 +14,8 @@ import java.nio.file.StandardCopyOption; import java.nio.file.attribute.PosixFilePermission; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -38,36 +36,51 @@ public DownloadedCloudflared(CloudflaredDownload download, String version) { public static CompletableFuture tryCreate() { if(VERSION_FILE.exists()) { try { - var version = Modflared.GSON.fromJson(new InputStreamReader(new FileInputStream(VERSION_FILE)), DownloadedCloudflared.class); - if(version != null) return CompletableFuture.completedFuture(version); + DownloadedCloudflared version = Modflared.GSON.fromJson(new InputStreamReader(new FileInputStream(VERSION_FILE)), DownloadedCloudflared.class); + if(version != null) { + return CompletableFuture.completedFuture(version); + } } catch (Throwable throwable) { Modflared.LOGGER.error("Failed to load existing version file creating new one...", throwable); } } - return GithubAPI.requestLatestVersion().thenApply(latestVersion -> new DownloadedCloudflared(CloudflaredDownload.find(), latestVersion)); + return GithubAPI.requestLatestVersion().thenApply(new java.util.function.Function() { + @Override + public Cloudflared apply(String latestVersion) { + return new DownloadedCloudflared(CloudflaredDownload.find(), latestVersion); + } + }); } @Override public CompletableFuture prepare() { if(isInstalled()) { - CompletableFuture completableFuture = new CompletableFuture<>(); - isUptoDate().whenComplete((pair, throwable) -> { - if (throwable != null) { - Modflared.LOGGER.error("Failed to check for updates", throwable); - TunnelManager.displayErrorToast(); - completableFuture.complete(null); - } else { - if(!pair.getFirst()) { - Modflared.LOGGER.info("Update detected updating..."); - version = pair.getSecond(); - downloadAndSaveInfo().whenComplete((unused, throwable1) -> { - if (throwable1 != null) Modflared.LOGGER.error("Failed to download update", throwable1); - TunnelManager.displayErrorToast(); - completableFuture.complete(null); - }); - } else { - Modflared.LOGGER.info("Cloudflared has no updates :)"); + final CompletableFuture completableFuture = new CompletableFuture(); + requestLatestVersion().whenComplete(new java.util.function.BiConsumer() { + @Override + public void accept(String latestVersion, Throwable throwable) { + if (throwable != null) { + Modflared.LOGGER.error("Failed to check for updates", throwable); + TunnelManager.logSetupError(); completableFuture.complete(null); + } else { + if(!latestVersion.equals(DownloadedCloudflared.this.version)) { + Modflared.LOGGER.info("Update detected updating..."); + DownloadedCloudflared.this.version = latestVersion; + downloadAndSaveInfo().whenComplete(new java.util.function.BiConsumer() { + @Override + public void accept(Void unused, Throwable throwable1) { + if (throwable1 != null) { + Modflared.LOGGER.error("Failed to download update", throwable1); + TunnelManager.logSetupError(); + } + completableFuture.complete(null); + } + }); + } else { + Modflared.LOGGER.info("Cloudflared has no updates :)"); + completableFuture.complete(null); + } } } }); @@ -78,22 +91,25 @@ public CompletableFuture prepare() { } @Override - public String[] buildCommand(RunningTunnel.@NotNull Access access) { - var command = access.command(createBinaryRef().getName(), true); + public String[] buildCommand(RunningTunnel.Access access) { + String[] command = access.command(createBinaryRef().getName(), true); Modflared.LOGGER.info(Arrays.toString(command).replace(",","")); - if (Platform.get() == Platform.WINDOWS) { + if (RunningTunnel.isWindows()) { command[0] = "\"" + TunnelManager.DATA_FOLDER.getAbsolutePath() + "\\" + command[0] + "\""; } return command; } - private @NotNull CompletableFuture downloadAndSaveInfo() { - return downloadFile().thenAccept(unused -> { - try { - save(); - } catch (Throwable throwable) { - Modflared.LOGGER.error("Failed to save current installed version", throwable); - TunnelManager.displayErrorToast(); + private CompletableFuture downloadAndSaveInfo() { + return downloadFile().thenAccept(new java.util.function.Consumer() { + @Override + public void accept(Void unused) { + try { + save(); + } catch (Throwable throwable) { + Modflared.LOGGER.error("Failed to save current installed version", throwable); + TunnelManager.logSetupError(); + } } }); } @@ -102,48 +118,50 @@ public boolean isInstalled() { return createBinaryRef().exists() && VERSION_FILE.exists(); } - @Contract(" -> new") - public @NotNull File createBinaryRef() { + public File createBinaryRef() { return new File(TunnelManager.DATA_FOLDER, download.fileName()); } - public CompletableFuture> isUptoDate() { - return GithubAPI.requestLatestVersion().thenApply(latestVersion -> new Pair<>(latestVersion.equals(version), latestVersion)); + public CompletableFuture requestLatestVersion() { + return GithubAPI.requestLatestVersion(); } - public @NotNull CompletableFuture downloadFile() { - return GithubAPI.requestFileHash(download.downloadFile()).thenAcceptAsync(expected -> { - try { - for (int i = 0; i < 4; i++) { - Modflared.LOGGER.info("Downloading cloudflared version {} from github. Attempt: {}", version, i + 1); - var downloadedFile = syncDownloadFile(); - Modflared.LOGGER.info("Downloaded file preparing cloudflared binary..."); - var file = new File(TunnelManager.DATA_FOLDER, download.fileName()); - prepareFile(downloadedFile, file); - - // Check if file is corrupt - Modflared.LOGGER.info("Checking file integrity"); - var provided = GithubAPI.FileHash.computeHash(file); - if(expected.compareTo(provided)) { - Modflared.LOGGER.info("Download finished of cloudflared version {}!", version); - return; - } else { - Modflared.LOGGER.warn("This downloaded file does not match with the file hash provided on GitHub."); - Modflared.LOGGER.warn("Expected {}, Provided: {}", expected.hash(), provided.hash()); - - file.delete(); + public CompletableFuture downloadFile() { + return GithubAPI.requestFileHash(download.downloadFile()).thenAcceptAsync(new java.util.function.Consumer() { + @Override + public void accept(GithubAPI.FileHash expected) { + try { + for (int i = 0; i < 4; i++) { + Modflared.LOGGER.info("Downloading cloudflared version " + version + " from github. Attempt: " + (i + 1)); + File downloadedFile = syncDownloadFile(); + Modflared.LOGGER.info("Downloaded file preparing cloudflared binary..."); + File file = new File(TunnelManager.DATA_FOLDER, download.fileName()); + prepareFile(downloadedFile, file); + + // Check if file is corrupt + Modflared.LOGGER.info("Checking file integrity"); + GithubAPI.FileHash provided = GithubAPI.FileHash.computeHash(file); + if(expected.compareTo(provided)) { + Modflared.LOGGER.info("Download finished of cloudflared version " + version + "!"); + return; + } else { + Modflared.LOGGER.warn("This downloaded file does not match with the file hash provided on GitHub."); + Modflared.LOGGER.warn("Expected " + expected.hash() + ", Provided: " + provided.hash()); + file.delete(); + } } + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Download interrupted", exception); + } catch (Exception exception) { + throw new IllegalStateException("Failed to download cloudflared binary", exception); } - } catch (InterruptedException exception) { - throw new IllegalStateException("Error while unpacking MacOS cloudflared download", exception); - } catch (Exception exception) { - throw new IllegalStateException("Failed to download cloudflared binary", exception); + throw new IllegalStateException("Modflared failed 4 times to download cloudflared from github. Please check your internet connection"); } - throw new IllegalStateException("Modflared failed 5 times to download cloudflared from github. Please check your internet connection"); }, Modflared.EXECUTOR); } - private @NotNull File syncDownloadFile() throws IOException, InterruptedException { + private File syncDownloadFile() throws IOException, InterruptedException { File output = new File(TunnelManager.DATA_FOLDER, UUID.randomUUID().toString()); if(!output.getParentFile().exists()) output.getParentFile().mkdirs(); if(!output.exists()) output.createNewFile(); @@ -159,25 +177,23 @@ public CompletableFuture> isUptoDate() { return output; } - private void prepareFile(@NotNull File downloadedFile, File targetFile) throws IOException, InterruptedException { - var platform = Platform.get(); - - if(platform == Platform.MACOSX) { - var workingDirectory = downloadedFile.getParentFile().toPath(); + private void prepareFile(File downloadedFile, File targetFile) throws IOException, InterruptedException { + if(download.osName().contains("mac os x")) { + Path workingDirectory = downloadedFile.getParentFile().toPath(); runCommand(workingDirectory, "tar", "-xzf", downloadedFile.getName()); Files.move(workingDirectory.resolve("cloudflared"), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } else { Files.move(downloadedFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } - if(platform == Platform.LINUX || platform == Platform.MACOSX) { + if(!RunningTunnel.isWindows()) { makeExecutable(targetFile.toPath()); } downloadedFile.delete(); } - private void runCommand(@NotNull Path workingDirectory, String... command) throws IOException, InterruptedException { + private void runCommand(Path workingDirectory, String... command) throws IOException, InterruptedException { ProcessBuilder processBuilder = new ProcessBuilder(command) .directory(workingDirectory.toFile()) .redirectErrorStream(true); @@ -193,16 +209,16 @@ private void runCommand(@NotNull Path workingDirectory, String... command) throw } } - private void makeExecutable(@NotNull Path path) throws IOException { + private void makeExecutable(Path path) throws IOException { try { - var permissions = Files.getPosixFilePermissions(path); + Set permissions = new HashSet(Files.getPosixFilePermissions(path)); permissions.add(PosixFilePermission.OWNER_EXECUTE); permissions.add(PosixFilePermission.GROUP_EXECUTE); permissions.add(PosixFilePermission.OTHERS_EXECUTE); Files.setPosixFilePermissions(path, permissions); } catch (UnsupportedOperationException exception) { // Fallback (non-POSIX) - var file = path.toFile(); + File file = path.toFile(); if(!file.setExecutable(true, false)) { throw new IOException("Failed to set executable bit on " + file.getName()); } @@ -210,7 +226,7 @@ private void makeExecutable(@NotNull Path path) throws IOException { } private void save() throws IOException { - Files.writeString(VERSION_FILE.toPath(), Modflared.GSON.toJson(this), StandardCharsets.UTF_8); + Files.write(VERSION_FILE.toPath(), Modflared.GSON.toJson(this).getBytes(StandardCharsets.UTF_8)); } } diff --git a/src/main/java/dev/httxrafa/modflared/binary/local/LocalCloudflared.java b/src/main/java/dev/httxrafa/modflared/binary/local/LocalCloudflared.java index 7590bdc..97d57f5 100644 --- a/src/main/java/dev/httxrafa/modflared/binary/local/LocalCloudflared.java +++ b/src/main/java/dev/httxrafa/modflared/binary/local/LocalCloudflared.java @@ -3,8 +3,6 @@ import dev.httxrafa.modflared.Modflared; import dev.httxrafa.modflared.binary.Cloudflared; import dev.httxrafa.modflared.tunnel.RunningTunnel; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; import java.io.InputStreamReader; @@ -22,20 +20,20 @@ public CompletableFuture prepare() { } @Override - public String[] buildCommand(RunningTunnel.@NotNull Access access) { + public String[] buildCommand(RunningTunnel.Access access) { return access.command("cloudflared", false); } - public static @Nullable Cloudflared tryCreate() { + public static Cloudflared tryCreate() { // Check if cloudflared is already installed on the system try { - var builder = new ProcessBuilder("cloudflared", "--version"); - var process = builder.start(); - var reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + ProcessBuilder builder = new ProcessBuilder("cloudflared", "--version"); + Process process = builder.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String versionString = reader.readLine(); String version = versionString.split(" ")[2]; - Modflared.LOGGER.info("Cloudflared output: {}", versionString); - Modflared.LOGGER.info("Cloudflared version {} is already installed on the system", version); + Modflared.LOGGER.info("Cloudflared output: " + versionString); + Modflared.LOGGER.info("Cloudflared version " + version + " is already installed on the system"); return new LocalCloudflared(version); } catch (Throwable ignored) { Modflared.LOGGER.info("Cloudflared is not installed on the system. Downloading it if necessary..."); diff --git a/src/main/java/dev/httxrafa/modflared/github/GithubAPI.java b/src/main/java/dev/httxrafa/modflared/github/GithubAPI.java index 47a73a4..cecff43 100644 --- a/src/main/java/dev/httxrafa/modflared/github/GithubAPI.java +++ b/src/main/java/dev/httxrafa/modflared/github/GithubAPI.java @@ -7,9 +7,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import dev.httxrafa.modflared.Modflared; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; import java.io.File; import java.io.IOException; @@ -19,7 +16,8 @@ import java.net.URI; import java.net.URL; import java.net.URLConnection; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -39,57 +37,86 @@ public class GithubAPI { } } - @Contract(" -> new") - public static @NotNull CompletableFuture requestLatestVersion() { - return CompletableFuture.supplyAsync(() -> { - try { - return getJsonFromEndpoint(GITHUB_API_ENDPOINT).get("tag_name").getAsString(); - } catch (Throwable throwable) { - throw new IllegalStateException("Failed to get latest cloudflared version from github", throwable); + public static CompletableFuture requestLatestVersion() { + return CompletableFuture.supplyAsync(new java.util.function.Supplier() { + @Override + public String get() { + try { + return getJsonFromEndpoint(GITHUB_API_ENDPOINT).get("tag_name").getAsString(); + } catch (Throwable throwable) { + throw new IllegalStateException("Failed to get latest cloudflared version from github", throwable); + } } }, Modflared.EXECUTOR); } - @Contract("_ -> new") - public static @NotNull CompletableFuture requestFileHash(String filename) { - return CompletableFuture.supplyAsync(() -> { - try { - return extractHashes(getJsonFromEndpoint(GITHUB_API_ENDPOINT)).stream().filter(item -> item.file.equals(filename)).findFirst().orElseThrow(); - } catch (Throwable throwable) { - throw new IllegalStateException("Failed to get file hash from github", throwable); + public static CompletableFuture requestFileHash(String filename) { + return CompletableFuture.supplyAsync(new java.util.function.Supplier() { + @Override + public FileHash get() { + try { + List hashes = extractHashes(getJsonFromEndpoint(GITHUB_API_ENDPOINT)); + for (FileHash item : hashes) { + if (item.file().equals(filename)) { + return item; + } + } + throw new IllegalStateException("File hash not found for " + filename); + } catch (Throwable throwable) { + throw new IllegalStateException("Failed to get file hash from github", throwable); + } } }, Modflared.EXECUTOR); } - private static @NotNull @Unmodifiable List extractHashes(@NotNull JsonObject data) { - return Arrays.stream(data.get("body").getAsString().split("\n")).filter(item -> item.startsWith("cloudflared-") && item.contains(":")).map(item -> { - var fileData = item.split(":"); - return new FileHash(fileData[0].trim(), fileData[1].trim()); - }).toList(); + private static List extractHashes(JsonObject data) { + List hashes = new ArrayList(); + String[] lines = data.get("body").getAsString().split("\n"); + for (String line : lines) { + if (line.startsWith("cloudflared-") && line.contains(":")) { + String[] fileData = line.split(":"); + hashes.add(new FileHash(fileData[0].trim(), fileData[1].trim())); + } + } + return Collections.unmodifiableList(hashes); } - private static JsonObject getJsonFromEndpoint(@NotNull URL url) throws IOException { + private static JsonObject getJsonFromEndpoint(URL url) throws IOException { URLConnection connection = url.openConnection(); InputStream inputStream = connection.getInputStream(); - return JsonParser.parseReader(new InputStreamReader(inputStream)).getAsJsonObject(); + return new JsonParser().parse(new InputStreamReader(inputStream)).getAsJsonObject(); } - public record FileHash(String file, String hash) { + public static class FileHash { + private final String file; + private final String hash; + + public FileHash(String file, String hash) { + this.file = file; + this.hash = hash; + } + + public String file() { + return file; + } + + public String hash() { + return hash; + } public boolean compareTo(File file) throws IOException { return compareTo(computeHash(file)); } - public boolean compareTo(@NotNull FileHash hash) { + public boolean compareTo(FileHash hash) { return Objects.equals(this.hash, hash.hash()); } - public static @NotNull FileHash computeHash(File file) throws IOException { + public static FileHash computeHash(File file) throws IOException { ByteSource byteSource = Files.asByteSource(file); HashCode hashCode = byteSource.hash(Hashing.sha256()); return new FileHash(file.getName(), hashCode.toString()); } - } } diff --git a/src/main/java/dev/httxrafa/modflared/mixin/ConnectionMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/ConnectionMixin.java deleted file mode 100644 index 61d1a0c..0000000 --- a/src/main/java/dev/httxrafa/modflared/mixin/ConnectionMixin.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.httxrafa.modflared.mixin; - -import dev.httxrafa.modflared.Modflared; -import dev.httxrafa.modflared.interfaces.mixin.IConnection; -import dev.httxrafa.modflared.tunnel.RunningTunnel; -import net.minecraft.network.Connection; -import net.minecraft.network.chat.Component; -import org.spongepowered.asm.mixin.*; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Implements(@Interface(iface = IConnection.class, prefix = "connection$")) -@Mixin(Connection.class) -public abstract class ConnectionMixin implements IConnection { - - @Unique - private RunningTunnel modflared$runningTunnel = null; - - /* Replaced by MultiplayerServerListPingerMixin - @Redirect(method = "connect(Ljava/net/InetSocketAddress;ZLnet/minecraft/util/profiler/PerformanceLog;)Lnet/minecraft/network/ClientConnection;", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/ClientConnection;connect(Ljava/net/InetSocketAddress;ZLnet/minecraft/network/ClientConnection;)Lio/netty/channel/ChannelFuture;")) - private static ChannelFuture connect(@NotNull InetSocketAddress address, boolean useEpoll, ClientConnection connection) { - return ClientConnection.connect(Modflared.TUNNEL_MANAGER.handleConnect(address, connection).address(), useEpoll, connection); - }*/ - - @Inject(method = "disconnect*", at = @At("TAIL")) - public void disconnect(Component disconnectReason, CallbackInfo callbackInfo) { - synchronized(this) { - if(this.modflared$runningTunnel != null) { - Modflared.TUNNEL_MANAGER.closeTunnel(this.modflared$runningTunnel); - this.modflared$runningTunnel = null; - } - } - } - - @Intrinsic - public void connection$setRunningTunnel(RunningTunnel runningTunnel) { - this.modflared$runningTunnel = runningTunnel; - } - -} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/ModflaredMixinLoader.java b/src/main/java/dev/httxrafa/modflared/mixin/ModflaredMixinLoader.java new file mode 100644 index 0000000..798a582 --- /dev/null +++ b/src/main/java/dev/httxrafa/modflared/mixin/ModflaredMixinLoader.java @@ -0,0 +1,41 @@ +package dev.httxrafa.modflared.mixin; + +import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; +import org.spongepowered.asm.launch.MixinBootstrap; +import org.spongepowered.asm.mixin.Mixins; + +import java.util.Map; + +@IFMLLoadingPlugin.MCVersion("1.12.2") +@IFMLLoadingPlugin.Name("ModflaredMixinLoader") +public class ModflaredMixinLoader implements IFMLLoadingPlugin { + + public ModflaredMixinLoader() { + MixinBootstrap.init(); + Mixins.addConfiguration("modflared.mixins.json"); + } + + @Override + public String[] getASMTransformerClass() { + return new String[0]; + } + + @Override + public String getModContainerClass() { + return null; + } + + @Override + public String getSetupClass() { + return null; + } + + @Override + public void injectData(Map data) { + } + + @Override + public String getAccessTransformerClass() { + return null; + } +} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/NetworkManagerMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/NetworkManagerMixin.java new file mode 100644 index 0000000..cd25da5 --- /dev/null +++ b/src/main/java/dev/httxrafa/modflared/mixin/NetworkManagerMixin.java @@ -0,0 +1,37 @@ +package dev.httxrafa.modflared.mixin; + +import dev.httxrafa.modflared.Modflared; +import dev.httxrafa.modflared.interfaces.mixin.IConnection; +import dev.httxrafa.modflared.tunnel.RunningTunnel; +import net.minecraft.network.NetworkManager; +import org.spongepowered.asm.mixin.Implements; +import org.spongepowered.asm.mixin.Interface; +import org.spongepowered.asm.mixin.Intrinsic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Implements(@Interface(iface = IConnection.class, prefix = "connection$")) +@Mixin(NetworkManager.class) +public abstract class NetworkManagerMixin implements IConnection { + + @Unique + private RunningTunnel modflared$runningTunnel; + + @Inject(method = "closeChannel", at = @At("TAIL")) + private void modflared$closeTunnelOnDisconnect(CallbackInfo callbackInfo) { + synchronized (this) { + if (modflared$runningTunnel != null) { + Modflared.TUNNEL_MANAGER.closeTunnel(modflared$runningTunnel); + modflared$runningTunnel = null; + } + } + } + + @Intrinsic + public void connection$setRunningTunnel(RunningTunnel runningTunnel) { + this.modflared$runningTunnel = runningTunnel; + } +} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/ConnectScreenMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/ConnectScreenMixin.java deleted file mode 100644 index c28a3e0..0000000 --- a/src/main/java/dev/httxrafa/modflared/mixin/client/ConnectScreenMixin.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.httxrafa.modflared.mixin.client; - -import dev.httxrafa.modflared.interfaces.mixin.IConnectScreen; -import dev.httxrafa.modflared.tunnel.TunnelStatus; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.ConnectScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.*; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Implements(@Interface(iface = IConnectScreen.class, prefix = "connectScreen$")) -@Mixin(ConnectScreen.class) -public abstract class ConnectScreenMixin extends Screen implements IConnectScreen { - - protected ConnectScreenMixin(Component title) { - super(title); - } - - @Unique - @Nullable - public TunnelStatus modflared$status; - - @Intrinsic - public void connectScreen$setStatus(TunnelStatus status) { - this.modflared$status = status; - } - - @Shadow - private Component status; - - @Inject(method = "render", at = @At("TAIL")) - public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { - // This screen starts drawing before the connection is established, so we need to check if the status is null - // We're also checking if the status is the default "Connecting..." status, because we know we've connected to the server - // when the status changes - if (this.modflared$status == null || !status.equals(Component.translatable("connect.connecting"))) return; - - int y = this.height / 2 - 50; - // Connecting Text is drawn at y = this.height / 2 - 50 - y += 10; - - for (Component status : this.modflared$status.generateFeedback()) { - y += 10; - graphics.drawCenteredString(this.font, status, this.width / 2, y, 16777215); - } - } - -} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/ConnectScreenRunnableMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/ConnectScreenRunnableMixin.java deleted file mode 100644 index f68b98c..0000000 --- a/src/main/java/dev/httxrafa/modflared/mixin/client/ConnectScreenRunnableMixin.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.httxrafa.modflared.mixin.client; - -import dev.httxrafa.modflared.Modflared; -import dev.httxrafa.modflared.interfaces.mixin.IConnectScreen; -import dev.httxrafa.modflared.tunnel.TunnelStatus; -import io.netty.channel.ChannelFuture; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.ConnectScreen; -import net.minecraft.network.Connection; -import net.minecraft.server.network.EventLoopGroupHolder; -import org.jetbrains.annotations.NotNull; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import java.net.InetSocketAddress; - -@Mixin(targets = "net.minecraft.client.gui.screens.ConnectScreen$1") -public class ConnectScreenRunnableMixin { - - @Redirect(method = "run", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;connect(Ljava/net/InetSocketAddress;Lnet/minecraft/server/network/EventLoopGroupHolder;Lnet/minecraft/network/Connection;)Lio/netty/channel/ChannelFuture;")) - private @NotNull ChannelFuture connect(@NotNull InetSocketAddress address, EventLoopGroupHolder holder, Connection connection) { - var status = Modflared.TUNNEL_MANAGER.handleConnect(address); - Modflared.TUNNEL_MANAGER.prepareConnection(status, connection); - - var currentScreen = Minecraft.getInstance().screen; - if (currentScreen instanceof ConnectScreen connectScreen) { - ((IConnectScreen) connectScreen).setStatus(status); - } - - return Connection.connect(status.state() == TunnelStatus.State.USE ? status.runningTunnel().access().tunnelAddress() : address, holder, connection); - } - -} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/GuiConnectingMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/GuiConnectingMixin.java new file mode 100644 index 0000000..8440547 --- /dev/null +++ b/src/main/java/dev/httxrafa/modflared/mixin/client/GuiConnectingMixin.java @@ -0,0 +1,41 @@ +package dev.httxrafa.modflared.mixin.client; + +import dev.httxrafa.modflared.interfaces.mixin.IConnectScreen; +import dev.httxrafa.modflared.tunnel.TunnelStatus; +import net.minecraft.client.multiplayer.GuiConnecting; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import org.spongepowered.asm.mixin.Implements; +import org.spongepowered.asm.mixin.Interface; +import org.spongepowered.asm.mixin.Intrinsic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Implements(@Interface(iface = IConnectScreen.class, prefix = "connectScreen$")) +@Mixin(GuiConnecting.class) +public abstract class GuiConnectingMixin extends GuiScreen implements IConnectScreen { + + @Unique + private TunnelStatus modflared$status; + + @Inject(method = "drawScreen", at = @At("TAIL")) + private void modflared$drawTunnelStatus(int mouseX, int mouseY, float partialTicks, CallbackInfo callbackInfo) { + if (modflared$status == null) { + return; + } + + int y = this.height / 2 - 50; + for (ITextComponent status : modflared$status.generateFeedback()) { + y += 10; + this.drawCenteredString(this.fontRenderer, status.getFormattedText(), this.width / 2, y, 16777215); + } + } + + @Intrinsic + public void connectScreen$setStatus(TunnelStatus status) { + this.modflared$status = status; + } +} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/GuiConnectingThreadMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/GuiConnectingThreadMixin.java new file mode 100644 index 0000000..23af14e --- /dev/null +++ b/src/main/java/dev/httxrafa/modflared/mixin/client/GuiConnectingThreadMixin.java @@ -0,0 +1,51 @@ +package dev.httxrafa.modflared.mixin.client; + +import dev.httxrafa.modflared.Modflared; +import dev.httxrafa.modflared.interfaces.mixin.IConnectScreen; +import dev.httxrafa.modflared.tunnel.TunnelStatus; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.GuiConnecting; +import net.minecraft.network.NetworkManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +@Mixin(targets = "net.minecraft.client.multiplayer.GuiConnecting$1") +public class GuiConnectingThreadMixin { + + @Redirect( + method = "run", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/NetworkManager;createNetworkManagerAndConnect(Ljava/net/InetAddress;IZ)Lnet/minecraft/network/NetworkManager;" + ) + ) + private NetworkManager modflared$routeDirectConnect(InetAddress address, int port, boolean useNativeTransport) throws UnknownHostException { + InetSocketAddress original = new InetSocketAddress(address, port); + TunnelStatus status = Modflared.TUNNEL_MANAGER.handleConnect(original); + + Minecraft minecraft = Minecraft.getMinecraft(); + if (minecraft.currentScreen instanceof GuiConnecting) { + ((IConnectScreen) minecraft.currentScreen).setStatus(status); + } + + InetSocketAddress target; + if (status.getState() == TunnelStatus.State.USE && status.getRunningTunnel() != null) { + target = status.getRunningTunnel().getAccess().getTunnelAddress(); + } else { + target = original; + } + + NetworkManager manager = NetworkManager.createNetworkManagerAndConnect( + InetAddress.getByName(target.getHostString()), + target.getPort(), + useNativeTransport + ); + Modflared.TUNNEL_MANAGER.prepareConnection(status, manager); + return manager; + } +} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/OnlineServerEntryMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/OnlineServerEntryMixin.java deleted file mode 100644 index 6962bf5..0000000 --- a/src/main/java/dev/httxrafa/modflared/mixin/client/OnlineServerEntryMixin.java +++ /dev/null @@ -1,48 +0,0 @@ -package dev.httxrafa.modflared.mixin.client; - -import dev.httxrafa.modflared.Modflared; -import dev.httxrafa.modflared.interfaces.mixin.IServerData; -import dev.httxrafa.modflared.tunnel.TunnelStatus; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList; -import net.minecraft.client.multiplayer.ServerData; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(ServerSelectionList.OnlineServerEntry.class) -public abstract class OnlineServerEntryMixin extends ServerSelectionList.Entry { - - @Shadow @Final private ServerData serverData; - - @Unique - private static final Identifier MODFLARED_INDICATOR_TEXTURE = Identifier.fromNamespaceAndPath(Modflared.MOD_ID, "icon/indicator"); - - @Inject(method = "renderContent", at = @At("TAIL")) - public void renderContent(GuiGraphics graphics, int mouseX, int mouseY, boolean hovered, float deltaTicks, CallbackInfo callbackInfo) { - var tunnelStatus = ((IServerData) serverData).getTunnelStatus(); - if(tunnelStatus != null && tunnelStatus.state() == TunnelStatus.State.USE) { - int xOffset = this.getContentWidth() - 15; - int yOffset = 10 + 1; - int x = this.getContentX(); - int y = this.getContentY(); - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, MODFLARED_INDICATOR_TEXTURE, x + xOffset, y + yOffset, 10, 10); - - // Tooltip - int l = mouseX - x; - int m = mouseY - y; - if (l >= this.getContentWidth() - 15 && l <= this.getContentWidth() - 5 && m >= 9 && m <= 9 + 10) { - graphics.setTooltipForNextFrame(Component.translatable("gui.multiplayer.tunnel.status.0").withStyle(ChatFormatting.AQUA), mouseX, mouseY); - } - } - } - -} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/ServerPingerMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/ServerPingerMixin.java new file mode 100644 index 0000000..8cad07a --- /dev/null +++ b/src/main/java/dev/httxrafa/modflared/mixin/client/ServerPingerMixin.java @@ -0,0 +1,51 @@ +package dev.httxrafa.modflared.mixin.client; + +import dev.httxrafa.modflared.Modflared; +import dev.httxrafa.modflared.interfaces.mixin.IServerData; +import dev.httxrafa.modflared.tunnel.TunnelStatus; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.network.ServerPinger; +import net.minecraft.network.NetworkManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +@Mixin(ServerPinger.class) +public abstract class ServerPingerMixin { + + @Redirect( + method = "ping", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/NetworkManager;createNetworkManagerAndConnect(Ljava/net/InetAddress;IZ)Lnet/minecraft/network/NetworkManager;" + ) + ) + private NetworkManager modflared$routeServerPing(InetAddress address, int port, boolean useNativeTransport, ServerData data) { + InetSocketAddress original = new InetSocketAddress(address, port); + TunnelStatus status = Modflared.TUNNEL_MANAGER.handleConnect(original); + ((IServerData) data).setTunnelStatus(status); + + InetSocketAddress target; + if (status.getState() == TunnelStatus.State.USE && status.getRunningTunnel() != null) { + target = status.getRunningTunnel().getAccess().getTunnelAddress(); + } else { + target = original; + } + + try { + NetworkManager manager = NetworkManager.createNetworkManagerAndConnect( + InetAddress.getByName(target.getHostString()), + target.getPort(), + useNativeTransport + ); + Modflared.TUNNEL_MANAGER.prepareConnection(status, manager); + return manager; + } catch (UnknownHostException exception) { + throw new RuntimeException("Failed to resolve ping target " + target, exception); + } + } +} diff --git a/src/main/java/dev/httxrafa/modflared/mixin/client/ServerStatusPingerMixin.java b/src/main/java/dev/httxrafa/modflared/mixin/client/ServerStatusPingerMixin.java deleted file mode 100644 index f817dbe..0000000 --- a/src/main/java/dev/httxrafa/modflared/mixin/client/ServerStatusPingerMixin.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.httxrafa.modflared.mixin.client; - -import dev.httxrafa.modflared.Modflared; -import dev.httxrafa.modflared.interfaces.mixin.IServerData; -import dev.httxrafa.modflared.tunnel.TunnelStatus; -import net.minecraft.client.multiplayer.ServerData; -import net.minecraft.client.multiplayer.ServerStatusPinger; -import net.minecraft.network.Connection; -import net.minecraft.server.network.EventLoopGroupHolder; -import net.minecraft.util.debugchart.LocalSampleLogger; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import java.net.InetSocketAddress; - -@Mixin(ServerStatusPinger.class) -public abstract class ServerStatusPingerMixin { - - @Redirect(method = "pingServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;connectToServer(Ljava/net/InetSocketAddress;Lnet/minecraft/server/network/EventLoopGroupHolder;Lnet/minecraft/util/debugchart/LocalSampleLogger;)Lnet/minecraft/network/Connection;")) - public Connection pingServer(InetSocketAddress address, EventLoopGroupHolder holder, LocalSampleLogger localSampleLogger, ServerData data) { - var result = Modflared.TUNNEL_MANAGER.handleConnect(address); - if(result.state() == TunnelStatus.State.USE) { - var connection = Connection.connectToServer(result.runningTunnel().access().tunnelAddress(), holder, localSampleLogger); - Modflared.TUNNEL_MANAGER.prepareConnection(result, connection); - ((IServerData) data).setTunnelStatus(result); - return connection; - } - return Connection.connectToServer(address, holder, localSampleLogger); - } - -} diff --git a/src/main/java/dev/httxrafa/modflared/tunnel/RunningTunnel.java b/src/main/java/dev/httxrafa/modflared/tunnel/RunningTunnel.java index a5bcadd..9358840 100644 --- a/src/main/java/dev/httxrafa/modflared/tunnel/RunningTunnel.java +++ b/src/main/java/dev/httxrafa/modflared/tunnel/RunningTunnel.java @@ -3,43 +3,68 @@ import dev.httxrafa.modflared.Modflared; import dev.httxrafa.modflared.binary.Cloudflared; import dev.httxrafa.modflared.tunnel.manager.TunnelManager; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.lwjgl.system.Platform; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import java.util.zip.CRC32; -public record RunningTunnel(Access access, Process process) { +public class RunningTunnel { - public static @NotNull CompletableFuture createTunnel(@NotNull Cloudflared binary, @NotNull Access access) { - var future = new CompletableFuture(); - Modflared.EXECUTOR.execute(() -> { - try { - ProcessBuilder processBuilder = new ProcessBuilder(binary.buildCommand(access)); - // Since LINUX, MACOSX, and WINDOWS are the only options, this will work to only set the directory for Linux and MacOS - if (Platform.get() != Platform.WINDOWS) { - processBuilder.directory(TunnelManager.DATA_FOLDER); - } - processBuilder.redirectErrorStream(true); - var process = processBuilder.start(); - - var reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - - String line; - while ((line = reader.readLine()) != null) { - TunnelManager.CLOUDFLARE_LOGGER.info(line); - if (line.contains("Start Websocket listener")) { - // Wait for the websocket to start (this is a hacky solution, but I don't really see a better way) - Thread.sleep(250); - future.complete(new RunningTunnel(access, process)); // Tunnel was started. Return running tunnel to minecraft client + private final Access access; + private final Process process; + + public RunningTunnel(Access access, Process process) { + this.access = access; + this.process = process; + } + + public Access getAccess() { + return access; + } + + public Process getProcess() { + return process; + } + + public static CompletableFuture createTunnel(final Cloudflared binary, final Access access) { + final CompletableFuture future = new CompletableFuture(); + Modflared.EXECUTOR.execute(new Runnable() { + @Override + public void run() { + try { + ProcessBuilder processBuilder = new ProcessBuilder(binary.buildCommand(access)); + if (!isWindows()) { + processBuilder.directory(TunnelManager.DATA_FOLDER); + } + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); + + String line; + while ((line = reader.readLine()) != null) { + TunnelManager.CLOUDFLARE_LOGGER.info(line); + if (line.contains("Start Websocket listener")) { + Thread.sleep(250L); + future.complete(new RunningTunnel(access, process)); + } } + + if (!future.isDone()) { + future.completeExceptionally(new IOException("cloudflared exited before opening websocket listener")); + } + } catch (IOException e) { + Modflared.LOGGER.error("Failed to start cloudflared", e); + future.completeExceptionally(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Modflared.LOGGER.error("Interrupted while starting cloudflared", e); + future.completeExceptionally(e); } - } catch (IOException | InterruptedException exception) { - Modflared.LOGGER.error("Failed to start cloudflared", exception); - future.completeExceptionally(exception); } }); return future; @@ -49,27 +74,86 @@ public void closeTunnel() { process.destroy(); } - public record Access(String protocol, String hostname, InetSocketAddress tunnelAddress) { - @Contract("_ -> new") - public static @NotNull Access localWithRandomPort(String host) { + public static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("windows"); + } + + public static class Access { + private final String protocol; + private final String hostname; + private final InetSocketAddress tunnelAddress; + + public Access(String protocol, String hostname, InetSocketAddress tunnelAddress) { + this.protocol = protocol; + this.hostname = hostname; + this.tunnelAddress = tunnelAddress; + } + + public static Access localWithRandomPort(String host) { return new Access("tcp", host, new InetSocketAddress("127.0.0.1", computePort(host))); } - public String @NotNull [] command(@NotNull String fileName, boolean prefix) { - return new String[] {(prefix && Platform.get() != Platform.WINDOWS ? "./" : "") + fileName, "access", protocol, "--hostname", hostname, "--url", tunnelAddress.getHostString() + ":" + tunnelAddress.getPort()}; + public String[] command(String fileName, boolean prefix) { + String executable = prefix && !isWindows() ? "./" + fileName : fileName; + String host = hostname; + int destinationPort = -1; + int colonIndex = hostname.lastIndexOf(':'); + if (colonIndex > 0) { + host = hostname.substring(0, colonIndex); + try { + destinationPort = Integer.parseInt(hostname.substring(colonIndex + 1)); + } catch (NumberFormatException ignored) { + // Not a valid port, keep full string as hostname + host = hostname; + destinationPort = -1; + } + } + if (destinationPort > 0) { + return new String[] { + executable, + "access", + protocol, + "--hostname", + host, + "--destination", + host + ":" + destinationPort, + "--url", + tunnelAddress.getHostString() + ":" + tunnelAddress.getPort() + }; + } + return new String[] { + executable, + "access", + protocol, + "--hostname", + host, + "--url", + tunnelAddress.getHostString() + ":" + tunnelAddress.getPort() + }; } - public static int computePort(@NotNull String host) { - final int MIN_PORT = 25565; - final int MAX_PORT = 65530; - final int RANGE = MAX_PORT - MIN_PORT + 1; + public static int computePort(String host) { + final int minPort = 25565; + final int maxPort = 65530; + final int range = maxPort - minPort + 1; CRC32 crc32 = new CRC32(); - crc32.update(host.getBytes()); + crc32.update(host.getBytes(StandardCharsets.UTF_8)); long hash = crc32.getValue(); - return (int) ((hash % RANGE) + MIN_PORT); + return (int) ((hash % range) + minPort); } - } + public String getProtocol() { + return protocol; + } + + public String getHostname() { + return hostname; + } + + public InetSocketAddress getTunnelAddress() { + return tunnelAddress; + } + } } diff --git a/src/main/java/dev/httxrafa/modflared/tunnel/TunnelStatus.java b/src/main/java/dev/httxrafa/modflared/tunnel/TunnelStatus.java index bcb75eb..ec88a47 100644 --- a/src/main/java/dev/httxrafa/modflared/tunnel/TunnelStatus.java +++ b/src/main/java/dev/httxrafa/modflared/tunnel/TunnelStatus.java @@ -1,25 +1,41 @@ package dev.httxrafa.modflared.tunnel; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.Unmodifiable; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.text.TextFormatting; +import net.minecraft.util.text.ITextComponent; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -public record TunnelStatus(RunningTunnel runningTunnel, State state) { - - public @Unmodifiable List generateFeedback() { - return switch (state) { - case USE -> List.of( - Component.translatable("gui.tunnel.status.use").withStyle(ChatFormatting.AQUA) - ); - case DONT_USE -> List.of(); - case FAILED_TO_DETERMINE -> List.of( - Component.translatable("gui.tunnel.status.failed.0").withStyle(ChatFormatting.RED), - Component.translatable("gui.tunnel.status.failed.1").withStyle(ChatFormatting.RED), - Component.translatable("gui.tunnel.status.failed.2").withStyle(ChatFormatting.RED) - ); - }; +public class TunnelStatus { + + private final RunningTunnel runningTunnel; + private final State state; + + public TunnelStatus(RunningTunnel runningTunnel, State state) { + this.runningTunnel = runningTunnel; + this.state = state; + } + + public RunningTunnel getRunningTunnel() { + return runningTunnel; + } + + public State getState() { + return state; + } + + public List generateFeedback() { + List feedback = new ArrayList(); + if (state == State.USE) { + feedback.add(new TextComponentTranslation("gui.tunnel.status.use").setStyle(new net.minecraft.util.text.Style().setColor(TextFormatting.AQUA))); + } else if (state == State.FAILED_TO_DETERMINE) { + feedback.add(new TextComponentTranslation("gui.tunnel.status.failed.0").setStyle(new net.minecraft.util.text.Style().setColor(TextFormatting.RED))); + feedback.add(new TextComponentTranslation("gui.tunnel.status.failed.1").setStyle(new net.minecraft.util.text.Style().setColor(TextFormatting.RED))); + feedback.add(new TextComponentTranslation("gui.tunnel.status.failed.2").setStyle(new net.minecraft.util.text.Style().setColor(TextFormatting.RED))); + } + return Collections.unmodifiableList(feedback); } public enum State { @@ -27,5 +43,4 @@ public enum State { DONT_USE, FAILED_TO_DETERMINE } - } diff --git a/src/main/java/dev/httxrafa/modflared/tunnel/manager/TunnelManager.java b/src/main/java/dev/httxrafa/modflared/tunnel/manager/TunnelManager.java index 2b9818f..f321f6a 100644 --- a/src/main/java/dev/httxrafa/modflared/tunnel/manager/TunnelManager.java +++ b/src/main/java/dev/httxrafa/modflared/tunnel/manager/TunnelManager.java @@ -9,15 +9,10 @@ import dev.httxrafa.modflared.tunnel.RunningTunnel; import dev.httxrafa.modflared.tunnel.TunnelStatus; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.toasts.SystemToast; -import net.minecraft.client.multiplayer.resolver.ServerAddress; -import net.minecraft.network.Connection; -import net.minecraft.network.chat.Component; -import net.neoforged.fml.loading.FMLPaths; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.minecraft.client.multiplayer.ServerAddress; +import net.minecraft.network.NetworkManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import javax.naming.NamingException; import javax.naming.directory.Attribute; @@ -34,11 +29,11 @@ public class TunnelManager { - public static final File BASE_FOLDER = FMLPaths.GAMEDIR.get().resolve("modflared/").toFile(); - public static final File DATA_FOLDER = new File(BASE_FOLDER, "bin/"); + public static final File BASE_FOLDER = new File(Minecraft.getMinecraft().gameDir, "modflared"); + public static final File DATA_FOLDER = new File(BASE_FOLDER, "bin"); public static final File FORCED_TUNNELS_FILE = new File(BASE_FOLDER, "forced_tunnels.json"); - public static final Logger CLOUDFLARE_LOGGER = LoggerFactory.getLogger("Cloudflared"); + public static final Logger CLOUDFLARE_LOGGER = LogManager.getLogger("Cloudflared"); private final AtomicReference cloudflared = new AtomicReference<>(); private final List forcedTunnels = new ArrayList<>(); @@ -50,10 +45,10 @@ public void initDirectories() { } public RunningTunnel createTunnel(String host) { - var binary = this.cloudflared.get(); + Cloudflared binary = this.cloudflared.get(); if (binary != null) { Modflared.LOGGER.info("Starting tunnel to {}", host); - var process = binary.createTunnel(RunningTunnel.Access.localWithRandomPort(host)); + RunningTunnel process = binary.createTunnel(RunningTunnel.Access.localWithRandomPort(host)); if (process == null) return null; this.runningTunnels.add(process); return process; @@ -62,8 +57,8 @@ public RunningTunnel createTunnel(String host) { } } - public void closeTunnel(@NotNull RunningTunnel runningTunnel) { - Modflared.LOGGER.info("Stopping tunnel to {}", runningTunnel.access().tunnelAddress()); + public void closeTunnel(RunningTunnel runningTunnel) { + Modflared.LOGGER.info("Stopping tunnel to {}", runningTunnel.getAccess().getTunnelAddress()); this.runningTunnels.remove(runningTunnel); runningTunnel.closeTunnel(); } @@ -93,9 +88,11 @@ public void closeTunnels() { * @return The route to use for the tunnel, or null if the tunnel should not be used * @throws IOException If an error occurs while resolving the DNS TXT records */ - public @Nullable String shouldUseTunnel(String host) throws IOException { - if (forcedTunnels.stream().anyMatch(serverAddress -> serverAddress.getHost().equalsIgnoreCase(host))) { - return host; + public String shouldUseTunnel(String host) throws IOException { + for (ServerAddress serverAddress : forcedTunnels) { + if (serverAddress.getIP().equalsIgnoreCase(host)) { + return host; + } } // Check if the host is an IP address @@ -103,16 +100,16 @@ public void closeTunnels() { return null; } try { - var properties = new Properties(); + Properties properties = new Properties(); properties.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); InitialDirContext dirContext = new InitialDirContext(properties); Attributes attributes = dirContext.getAttributes(host, new String[]{"TXT"}); Attribute txtRecords = attributes.get("TXT"); if (txtRecords != null) { - var iterator = txtRecords.getAll(); + javax.naming.NamingEnumeration iterator = txtRecords.getAll(); while (iterator.hasMore()) { - var record = ((String) iterator.next()).replaceAll("\"", ""); + String record = ((String) iterator.next()).replaceAll("\"", ""); if (record.startsWith("cloudflared-route=")) { return record.replace("cloudflared-route=", ""); } else if (record.equals("cloudflared-use-tunnel")) { @@ -144,7 +141,9 @@ var record = ((String) iterator.next()).replaceAll("\"", ""); * @see How do you tell whether a string is an IP or a hostname */ private boolean isHost(String ip) { - if (ip.strip().equalsIgnoreCase("localhost")) return false; + if (ip.trim().equalsIgnoreCase("localhost")) { + return false; + } try { InetAddress[] ips = InetAddress.getAllByName(ip); // #getAllByName will return an InetAddress that is the same as the input if it is an IP address, @@ -155,14 +154,14 @@ private boolean isHost(String ip) { } } - public void prepareConnection(@NotNull TunnelStatus status, Connection connection) { - var tunnelConnection = (IConnection) connection; - if (status.runningTunnel() != null) { - tunnelConnection.setRunningTunnel(status.runningTunnel()); + public void prepareConnection(TunnelStatus status, NetworkManager connection) { + IConnection tunnelConnection = (IConnection) connection; + if (status.getRunningTunnel() != null) { + tunnelConnection.setRunningTunnel(status.getRunningTunnel()); } } - public TunnelStatus handleConnect(@NotNull InetSocketAddress address) { + public TunnelStatus handleConnect(InetSocketAddress address) { if(this.cloudflared.get() == null) { Modflared.LOGGER.warn("Modflared is not ready yet, ignoring all connections."); return new TunnelStatus(null, TunnelStatus.State.DONT_USE); @@ -186,18 +185,24 @@ public TunnelStatus handleConnect(@NotNull InetSocketAddress address) { } public void prepareBinary() { - Cloudflared.create().whenComplete((version, throwable) -> { - if (throwable != null) { - Modflared.LOGGER.error(throwable.getMessage(), throwable); - } else { - version.prepare().whenComplete((unused, throwable1) -> { - if (throwable1 != null) { - Modflared.LOGGER.error(throwable1.getMessage(), throwable1); - displayErrorToast(); - } else { - this.cloudflared.set(version); - } - }); + Cloudflared.create().whenComplete(new java.util.function.BiConsumer() { + @Override + public void accept(Cloudflared version, Throwable throwable) { + if (throwable != null) { + Modflared.LOGGER.error(throwable.getMessage(), throwable); + } else { + version.prepare().whenComplete(new java.util.function.BiConsumer() { + @Override + public void accept(Void unused, Throwable throwable1) { + if (throwable1 != null) { + Modflared.LOGGER.error(throwable1.getMessage(), throwable1); + logSetupError(); + } else { + TunnelManager.this.cloudflared.set(version); + } + } + }); + } } }); } @@ -209,16 +214,16 @@ public void loadForcedTunnels() { } try { - JsonArray entriesArray = JsonParser.parseReader( + JsonArray entriesArray = new JsonParser().parse( new InputStreamReader(new FileInputStream(FORCED_TUNNELS_FILE))).getAsJsonArray(); for (JsonElement jsonElement : entriesArray) { - var serverString = jsonElement.getAsString(); + String serverString = jsonElement.getAsString(); - if (!ServerAddress.isValidAddress(serverString)) { - Modflared.LOGGER.error("Invalid server address: {}", serverString); + ServerAddress serverAddress = parseServerAddress(serverString); + if (serverAddress == null) { continue; } - forcedTunnels.add(ServerAddress.parseString(serverString)); + forcedTunnels.add(serverAddress); } } catch (Exception exception) { Modflared.LOGGER.error("Failed to load forced tunnels", exception); @@ -226,12 +231,21 @@ public void loadForcedTunnels() { Modflared.LOGGER.info("Loaded {} forced tunnels", forcedTunnels.size()); for (ServerAddress serverAddress : forcedTunnels) { - Modflared.LOGGER.info(" - {}", serverAddress.getHost()); + Modflared.LOGGER.info(" - {}", serverAddress.getIP()); + } + } + + private ServerAddress parseServerAddress(String serverString) { + try { + return ServerAddress.fromString(serverString); + } catch (RuntimeException exception) { + Modflared.LOGGER.error("Invalid server address: " + serverString, exception); + return null; } } - public static void displayErrorToast() { - Minecraft.getInstance().getToastManager().addToast(new SystemToast(SystemToast.SystemToastId.PERIODIC_NOTIFICATION, Component.translatable("gui.toast.title.error"), Component.translatable("gui.toast.body.error"))); + public static void logSetupError() { + Modflared.LOGGER.error("Modflared setup failed. Check the log for cloudflared setup details."); } } diff --git a/src/main/resources/assets/modflared/lang/en_us.json b/src/main/resources/assets/modflared/lang/en_us.json deleted file mode 100644 index 5809935..0000000 --- a/src/main/resources/assets/modflared/lang/en_us.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "gui.tunnel.status.use": "Modflared has created a tunnel to the server...", - "gui.tunnel.status.failed.0": "Modflared failed to determine if tunnel should be used.", - "gui.tunnel.status.failed.1": "Assuming a tunnel should not be used...", - "gui.tunnel.status.failed.2": "See logs for more information.", - "gui.multiplayer.tunnel.status.0": "Modflared in use", - "gui.toast.title.error": "Modflared encountered an error.", - "gui.toast.body.error": "See logs for more information." -} \ No newline at end of file diff --git a/src/main/resources/assets/modflared/lang/en_us.lang b/src/main/resources/assets/modflared/lang/en_us.lang new file mode 100644 index 0000000..e079fca --- /dev/null +++ b/src/main/resources/assets/modflared/lang/en_us.lang @@ -0,0 +1,6 @@ +gui.tunnel.status.use=Using Cloudflare tunnel +gui.tunnel.status.failed.0=Modflared could not determine if a tunnel is required. +gui.tunnel.status.failed.1=The connection will continue without a tunnel. +gui.tunnel.status.failed.2=Add this server to forced_tunnels.json if it must use a tunnel. +gui.toast.title.error=Modflared error +gui.toast.body.error=Check the log for cloudflared setup details. diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..714bba6 --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,16 @@ +[ + { + "modid": "${mod_id}", + "name": "${mod_name}", + "description": "${mod_description}", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://github.com/HttpRafa/modflared", + "updateUrl": "", + "authorList": ["${mod_authors}"], + "credits": "", + "logoFile": "modflared.png", + "screenshots": [], + "dependencies": [] + } +] diff --git a/src/main/resources/modflared.mixins.json b/src/main/resources/modflared.mixins.json index e954371..503ebd2 100644 --- a/src/main/resources/modflared.mixins.json +++ b/src/main/resources/modflared.mixins.json @@ -1,21 +1,18 @@ { "required": true, + "minVersion": "0.8", "package": "dev.httxrafa.modflared.mixin", - "compatibilityLevel": "JAVA_21", + "compatibilityLevel": "JAVA_8", + "refmap": "modflared.refmap.json", "mixins": [ - "ConnectionMixin", - "client.ConnectScreenRunnableMixin" + "NetworkManagerMixin", + "client.GuiConnectingMixin", + "client.GuiConnectingThreadMixin", + "client.ServerDataMixin", + "client.ServerPingerMixin" ], + "client": [], "injectors": { "defaultRequire": 1 - }, - "overwrites": { - "requireAnnotations": true - }, - "client": [ - "client.ConnectScreenMixin", - "client.OnlineServerEntryMixin", - "client.ServerDataMixin", - "client.ServerStatusPingerMixin" - ] -} \ No newline at end of file + } +} diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml deleted file mode 100644 index 92b7958..0000000 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ /dev/null @@ -1,28 +0,0 @@ -license="${mod_license}" -issueTrackerURL="https://github.com/HttpRafa/modflared/issues" - -[[mods]] #mandatory -modId="${mod_id}" #mandatory -version="${mod_version}" #mandatory -displayName="${mod_name}" #mandatory -displayURL="https://github.com/HttpRafa/modflared" -logoFile="modflared.png" -authors="${mod_authors}" -description='''${mod_description}''' - -[[mixins]] -config="${mod_id}.mixins.json" - -[[dependencies.${mod_id}]] - modId="neoforge" #mandatory - type="required" #mandatory - versionRange="[${neo_version},)" #mandatory - ordering="NONE" - side="CLIENT" - -[[dependencies.${mod_id}]] - modId="minecraft" - type="required" - versionRange="${minecraft_version_range}" - ordering="NONE" - side="CLIENT" \ No newline at end of file diff --git a/src/test/java/dev/httxrafa/modflared/binary/download/CloudflaredDownloadTest.java b/src/test/java/dev/httxrafa/modflared/binary/download/CloudflaredDownloadTest.java new file mode 100644 index 0000000..c252b56 --- /dev/null +++ b/src/test/java/dev/httxrafa/modflared/binary/download/CloudflaredDownloadTest.java @@ -0,0 +1,29 @@ +package dev.httxrafa.modflared.binary.download; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class CloudflaredDownloadTest { + + @Test + public void findsWindowsAmd64Binary() { + CloudflaredDownload download = CloudflaredDownload.find("windows 10", "amd64"); + + assertEquals("cloudflared-windows-amd64.exe", download.fileName()); + assertEquals("cloudflared-windows-amd64.exe", download.downloadFile()); + } + + @Test + public void findsLinuxAmd64Binary() { + CloudflaredDownload download = CloudflaredDownload.find("linux", "amd64"); + + assertEquals("cloudflared-linux-amd64", download.fileName()); + assertEquals("cloudflared-linux-amd64", download.downloadFile()); + } + + @Test(expected = IllegalStateException.class) + public void rejectsUnsupportedPlatform() { + CloudflaredDownload.find("sunos", "sparc"); + } +} diff --git a/src/test/java/dev/httxrafa/modflared/tunnel/ForcedTunnelsJsonTest.java b/src/test/java/dev/httxrafa/modflared/tunnel/ForcedTunnelsJsonTest.java new file mode 100644 index 0000000..305bddb --- /dev/null +++ b/src/test/java/dev/httxrafa/modflared/tunnel/ForcedTunnelsJsonTest.java @@ -0,0 +1,24 @@ +package dev.httxrafa.modflared.tunnel; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.junit.Test; + +import java.io.InputStreamReader; + +import static org.junit.Assert.*; + +public class ForcedTunnelsJsonTest { + + @Test + public void testForcedTunnelsJsonIsValidArrayOfStrings() { + JsonElement element = new JsonParser().parse( + new InputStreamReader(getClass().getClassLoader().getResourceAsStream("forced_tunnels.json"))); + assertTrue("forced_tunnels.json must be a JSON array", element.isJsonArray()); + JsonArray array = element.getAsJsonArray(); + for (JsonElement entry : array) { + assertTrue("Each entry must be a string", entry.isJsonPrimitive() && entry.getAsJsonPrimitive().isString()); + } + } +} diff --git a/src/test/java/dev/httxrafa/modflared/tunnel/RunningTunnelAccessTest.java b/src/test/java/dev/httxrafa/modflared/tunnel/RunningTunnelAccessTest.java new file mode 100644 index 0000000..c970a85 --- /dev/null +++ b/src/test/java/dev/httxrafa/modflared/tunnel/RunningTunnelAccessTest.java @@ -0,0 +1,35 @@ +package dev.httxrafa.modflared.tunnel; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RunningTunnelAccessTest { + + @Test + public void computePortIsDeterministicForSameHost() { + int first = RunningTunnel.Access.computePort("example.com"); + int second = RunningTunnel.Access.computePort("example.com"); + + assertEquals(first, second); + } + + @Test + public void computePortStaysInMinecraftCompatibleRange() { + int port = RunningTunnel.Access.computePort("play.example.com"); + + assertTrue(port >= 25565); + assertTrue(port <= 65530); + } + + @Test + public void localAccessUsesLoopbackAndComputedPort() { + RunningTunnel.Access access = RunningTunnel.Access.localWithRandomPort("play.example.com"); + + assertEquals("tcp", access.getProtocol()); + assertEquals("play.example.com", access.getHostname()); + assertEquals("127.0.0.1", access.getTunnelAddress().getHostString()); + assertEquals(RunningTunnel.Access.computePort("play.example.com"), access.getTunnelAddress().getPort()); + } +}