Compare commits

...

9 commits

22 changed files with 318 additions and 97 deletions

8
.gitattributes vendored Normal file
View file

@ -0,0 +1,8 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf

View file

@ -1,4 +1,4 @@
Copyright (c) 2023, flashwave <me@flash.moe> Copyright (c) 2023-2024, flashwave <me@flash.moe>
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -13,6 +13,3 @@ gradlew downloadAssets
gradlew genSources gradlew genSources
gradlew build gradlew build
``` ```
## Known issues
- UniversalGraves doesn't seem to load skins properly. In true Java fashion, its codebase is a mystery to me and I feel like I'm going in circles.

View file

@ -1,5 +1,5 @@
plugins { plugins {
id 'fabric-loom' version '1.3-SNAPSHOT' id 'fabric-loom' version '1.7-SNAPSHOT'
id 'maven-publish' id 'maven-publish'
} }
@ -29,15 +29,15 @@ processResources {
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8" it.options.encoding = "UTF-8"
it.options.release = 17 it.options.release = 21
it.options.compilerArgs += ['-Xlint:deprecation', '-Xlint:unchecked'] it.options.compilerArgs += ['-Xlint:deprecation', '-Xlint:unchecked']
} }
java { java {
withSourcesJar() withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_21
} }
jar { jar {

View file

@ -4,11 +4,11 @@ org.gradle.parallel=true
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/develop # check these on https://fabricmc.net/develop
minecraft_version=1.20.1 minecraft_version=1.21
yarn_mappings=1.20.1+build.10 yarn_mappings=1.21+build.8
loader_version=0.14.22 loader_version=0.15.11
# Mod Properties # Mod Properties
mod_version=1.0.1 mod_version=1.1.0
maven_group=net.flashii.mcexts maven_group=net.flashii.mcexts
archives_base_name=flashii-extensions archives_base_name=flashii-extensions

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

19
gradlew vendored
View file

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -83,7 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -201,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View file

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View file

@ -0,0 +1,19 @@
package net.flashii.mcexts;
import com.mojang.authlib.yggdrasil.ProfileResult;
public class ProfileResultCache {
private static final long CACHE_TIMEOUT = 21600000;
public long readTime;
public ProfileResult value;
public ProfileResultCache(ProfileResult value) {
this.readTime = System.currentTimeMillis();
this.value = value;
}
public boolean needsRefresh() {
return readTime < System.currentTimeMillis() - CACHE_TIMEOUT;
}
}

View file

@ -4,12 +4,15 @@ import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.URL; import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.Base64; import java.util.Base64;
@ -18,6 +21,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import net.flashii.mcexts.Tools;
public class RPC { public class RPC {
private static final String DEFAULT_SECRET = "meow"; private static final String DEFAULT_SECRET = "meow";
@ -67,48 +71,55 @@ public class RPC {
// sigStr and bodyStr because sigStr must also include the query params if those are present // sigStr and bodyStr because sigStr must also include the query params if those are present
public static RPCPayload callRpc(String path, String sigStr, String bodyStr) public static RPCPayload callRpc(String path, String sigStr, String bodyStr)
throws GeneralSecurityException, IOException { throws GeneralSecurityException, IOException, InterruptedException {
boolean hasBody = bodyStr != null; boolean hasBody = bodyStr != null;
String time = getRequestTimestamp(); String time = getRequestTimestamp();
Tools.Log.info("[FII RPC] {} {} @ {}", hasBody ? "POST" : "GET", path, time);
String hash = createRequestSignature(time, URLs.getRpcPath(path), sigStr); String hash = createRequestSignature(time, URLs.getRpcPath(path), sigStr);
HttpURLConnection conn = (HttpURLConnection)(new URL(URLs.getRpcUrl(path))).openConnection(); URI url;
conn.setRequestMethod(hasBody ? "POST" : "GET"); try {
conn.setRequestProperty("X-Mince-Time", time); url = new URI(URLs.getRpcUrl(path));
conn.setRequestProperty("X-Mince-Hash", hash); } catch(URISyntaxException ex) {
// fuck it
if(hasBody) { throw new IOException(ex.getMessage());
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
try(OutputStream stream = conn.getOutputStream()) {
byte[] input = bodyStr.getBytes(StandardCharsets.UTF_8);
stream.write(input, 0, input.length);
}
} }
StringBuilder sb = new StringBuilder(); HttpClient client = HttpClient.newHttpClient();
try(BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
String line; .uri(url)
while((line = br.readLine()) != null) .header("X-Mince-Time", time)
sb.append(line); .header("X-Mince-Hash", hash);
}
return getGson().fromJson(sb.toString(), RPCPayload.class); if(hasBody)
requestBuilder.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(bodyStr, StandardCharsets.UTF_8));
else
requestBuilder.GET();
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != 200)
throw new IOException("RPC request failed: " + response.statusCode());
return getGson().fromJson(response.body(), RPCPayload.class);
} }
public static RPCPayload callRpc(String path, String paramStr) public static RPCPayload callRpc(String path, String paramStr)
throws GeneralSecurityException, IOException { throws GeneralSecurityException, IOException, InterruptedException {
return callRpc(path, paramStr, paramStr); return callRpc(path, paramStr, paramStr);
} }
public static RPCPayload callRpc(String path, Map<String, Object> params) public static RPCPayload callRpc(String path, Map<String, Object> params)
throws GeneralSecurityException, IOException { throws GeneralSecurityException, IOException, InterruptedException {
return callRpc(path, createParamString(params)); return callRpc(path, createParamString(params));
} }
public static RPCPayload postAuth(UUID id, String name, InetAddress remoteAddr) public static RPCPayload postAuth(UUID id, String name, InetAddress remoteAddr)
throws GeneralSecurityException, IOException { throws GeneralSecurityException, IOException, InterruptedException {
HashMap<String, Object> params = new HashMap<>(); HashMap<String, Object> params = new HashMap<>();
params.put("id", id); params.put("id", id);
params.put("name", name); params.put("name", name);
@ -118,7 +129,7 @@ public class RPC {
} }
public static RPCPayload postAuth(UUID id, String name, SocketAddress sockAddr) public static RPCPayload postAuth(UUID id, String name, SocketAddress sockAddr)
throws GeneralSecurityException, IOException { throws GeneralSecurityException, IOException, InterruptedException {
InetAddress remoteAddr = sockAddr instanceof InetSocketAddress InetAddress remoteAddr = sockAddr instanceof InetSocketAddress
? ((InetSocketAddress)sockAddr).getAddress() ? ((InetSocketAddress)sockAddr).getAddress()
: InetAddress.getLoopbackAddress(); : InetAddress.getLoopbackAddress();

View file

@ -0,0 +1,8 @@
package net.flashii.mcexts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Tools {
public static final Logger Log = LoggerFactory.getLogger("fiiexts");
}

View file

@ -1,16 +0,0 @@
package net.flashii.mcexts.mixin;
import net.flashii.mcexts.URLs;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(targets = "com.mojang.authlib.Environment$1")
public abstract class EnvironmentMixin {
@Inject(method = "getSessionHost()Ljava/lang/String;", at = @At("HEAD"), cancellable = true, remap = false)
private void getSessionHost(CallbackInfoReturnable<String> cir) {
cir.setReturnValue(URLs.getSessionHost());
cir.cancel();
}
}

View file

@ -0,0 +1,19 @@
package net.flashii.mcexts.mixin;
import net.minecraft.client.network.PlayerListEntry;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(PlayerListEntry.class)
public abstract class PlayerListEntryMixin {
@ModifyVariable(
method = "texturesSupplier(Lcom/mojang/authlib/GameProfile;)Ljava/util/function/Supplier;",
at = @At("STORE"),
ordinal = 0
)
private static boolean texturesSupplier(boolean bl) {
// always enable BL
return true;
}
}

View file

@ -8,8 +8,14 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(PlayerListHud.class) @Mixin(PlayerListHud.class)
public abstract class PlayerListHudMixin { public abstract class PlayerListHudMixin {
@Redirect(method = "render(Lnet/minecraft/client/gui/DrawContext;ILnet/minecraft/scoreboard/Scoreboard;Lnet/minecraft/scoreboard/ScoreboardObjective;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;isInSingleplayer()Z")) @Redirect(
public boolean redirectIsInSinglePlayer(MinecraftClient client) { method = "render(Lnet/minecraft/client/gui/DrawContext;ILnet/minecraft/scoreboard/Scoreboard;Lnet/minecraft/scoreboard/ScoreboardObjective;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/MinecraftClient;isInSingleplayer()Z"
)
)
public boolean isInSinglePlayer(MinecraftClient client) {
// always enable BL // always enable BL
return true; return true;
} }

View file

@ -6,6 +6,7 @@ import java.security.GeneralSecurityException;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import net.flashii.mcexts.RPC; import net.flashii.mcexts.RPC;
import net.flashii.mcexts.RPCPayload; import net.flashii.mcexts.RPCPayload;
import net.flashii.mcexts.Tools;
import net.minecraft.server.PlayerManager; import net.minecraft.server.PlayerManager;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Formatting; import net.minecraft.util.Formatting;
@ -16,7 +17,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerManager.class) @Mixin(PlayerManager.class)
public abstract class PlayerManagerMixin { public abstract class PlayerManagerMixin {
@Inject(method = "checkCanJoin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/text/Text;", at = @At("HEAD"), cancellable = true) @Inject(
method = "checkCanJoin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/text/Text;",
at = @At("HEAD"),
cancellable = true
)
private void checkCanJoin(SocketAddress sockAddr, GameProfile profile, CallbackInfoReturnable<Text> cir) { private void checkCanJoin(SocketAddress sockAddr, GameProfile profile, CallbackInfoReturnable<Text> cir) {
Text authText = null; Text authText = null;
@ -25,13 +30,15 @@ public abstract class PlayerManagerMixin {
if(!payload.is("auth:ok")) { if(!payload.is("auth:ok")) {
if(payload.is("error")) { if(payload.is("error")) {
authText = Text.literal( String errorCode = payload.getAttrStr("code");
payload.getAttrStr(payload.hasAttr("text") ? "text" : "code") String errorText = payload.hasAttr("text") ? payload.getAttrStr("text") : errorCode;
).formatted(Formatting.RED); Tools.Log.info("[FII AUTH] <{}:{}:{}> Error: {} {}", profile.getId(), profile.getName(), sockAddr, errorCode, errorText);
authText = Text.literal(errorText).formatted(Formatting.RED);
} else if(payload.is("auth:authorise")) { } else if(payload.is("auth:authorise")) {
String userName = payload.getAttrStr("user_name"); String userName = payload.getAttrStr("user_name");
int userColour = payload.hasAttr("user_colour") ? payload.getAttrInt("user_colour") : 0xFFFFFF; int userColour = payload.hasAttr("user_colour") ? payload.getAttrInt("user_colour") : 0xFFFFFF;
String url = payload.getAttrStr("url"); String url = payload.getAttrStr("url");
Tools.Log.info("[FII AUTH] <{}:{}:{}> Authorising: {} {} {}", profile.getId(), profile.getName(), sockAddr, userName, userColour, url);
authText = Text.literal("Welcome back, ") authText = Text.literal("Welcome back, ")
.append(Text.literal(userName).styled(style -> style.withColor(userColour).withBold(true))) .append(Text.literal(userName).styled(style -> style.withColor(userColour).withBold(true)))
@ -43,6 +50,7 @@ public abstract class PlayerManagerMixin {
} else if(payload.is("auth:link")) { } else if(payload.is("auth:link")) {
String code = payload.getAttrStr("code"); String code = payload.getAttrStr("code");
String url = payload.getAttrStr("url"); String url = payload.getAttrStr("url");
Tools.Log.info("[FII AUTH] <{}:{}:{}> Linking: {} {}", profile.getId(), profile.getName(), sockAddr, code, url);
authText = Text.literal("This seems to be the first time you're connecting!\n") authText = Text.literal("This seems to be the first time you're connecting!\n")
.append(Text.literal("Visit ")) .append(Text.literal("Visit "))
@ -51,15 +59,24 @@ public abstract class PlayerManagerMixin {
.append(Text.literal(code).formatted(Formatting.LIGHT_PURPLE)) .append(Text.literal(code).formatted(Formatting.LIGHT_PURPLE))
.append(Text.literal("\n\nAfter you've approved the attempt, connect to the server again and you should be good to go.")); .append(Text.literal("\n\nAfter you've approved the attempt, connect to the server again and you should be good to go."));
} else { } else {
Tools.Log.info("[FII AUTH] <{}:{}:{}> Unknown response: {}", profile.getId(), profile.getName(), sockAddr, payload.getName());
authText = Text.literal("Flashii authentication server returned an unknown response, yell at flashwave about this.").formatted(Formatting.RED); authText = Text.literal("Flashii authentication server returned an unknown response, yell at flashwave about this.").formatted(Formatting.RED);
} }
} else {
Tools.Log.info("[FII AUTH] <{}:{}:{}> Authentication successful!", profile.getId(), profile.getName(), sockAddr);
} }
} catch(IOException ex) { } catch(IOException ex) {
Tools.Log.error("[FII AUTH] <{}:{}:{}> IOException!!", profile.getId(), profile.getName(), sockAddr);
authText = Text.literal("Flashii authentication server failed to respond, yell at flashwave about this.").formatted(Formatting.RED); authText = Text.literal("Flashii authentication server failed to respond, yell at flashwave about this.").formatted(Formatting.RED);
ex.printStackTrace(); ex.printStackTrace();
} catch(GeneralSecurityException ex) { } catch(GeneralSecurityException ex) {
Tools.Log.error("[FII AUTH] <{}:{}:{}> GeneralSecurityException!!", profile.getId(), profile.getName(), sockAddr);
authText = Text.literal("Problem with request verification, yell at flashwave about this.").formatted(Formatting.RED); authText = Text.literal("Problem with request verification, yell at flashwave about this.").formatted(Formatting.RED);
ex.printStackTrace(); ex.printStackTrace();
} catch(InterruptedException ex) {
Tools.Log.error("[FII AUTH] <{}:{}:{}> InterruptedException!!", profile.getId(), profile.getName(), sockAddr);
authText = Text.literal("Problem with connecting to the Flashii authentication server, yell at flashwave about this.").formatted(Formatting.RED);
ex.printStackTrace();
} }
if(authText != null) { if(authText != null) {

View file

@ -13,7 +13,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ServerMetadata.class) @Mixin(ServerMetadata.class)
public abstract class ServerMetadataMixin { public abstract class ServerMetadataMixin {
@Inject(method = "description()Lnet/minecraft/text/Text;", at = @At("TAIL"), cancellable = true) @Inject(
method = "description()Lnet/minecraft/text/Text;",
at = @At("TAIL"),
cancellable = true
)
public void description(CallbackInfoReturnable<Text> cir) { public void description(CallbackInfoReturnable<Text> cir) {
String linesRaw = Config.getValue("MOTDLines.txt"); String linesRaw = Config.getValue("MOTDLines.txt");
if(linesRaw == null || linesRaw.isBlank()) if(linesRaw == null || linesRaw.isBlank())

View file

@ -9,7 +9,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(TextureUrlChecker.class) @Mixin(TextureUrlChecker.class)
public abstract class TextureUrlCheckerMixin { public abstract class TextureUrlCheckerMixin {
@Inject(method = "isAllowedTextureDomain(Ljava/lang/String;)Z", at = @At("HEAD"), cancellable = true, remap = false) @Inject(
method = "isAllowedTextureDomain(Ljava/lang/String;)Z",
at = @At("HEAD"),
cancellable = true,
remap = false
)
private static void isAllowedTextureDomain(String url, CallbackInfoReturnable<Boolean> cir) { private static void isAllowedTextureDomain(String url, CallbackInfoReturnable<Boolean> cir) {
if(url == null || url.startsWith(URLs.getTexturesHostPrefix())) { if(url == null || url.startsWith(URLs.getTexturesHostPrefix())) {
cir.setReturnValue(true); cir.setReturnValue(true);

View file

@ -0,0 +1,25 @@
package net.flashii.mcexts.mixin;
import com.mojang.authlib.Environment;
import com.mojang.authlib.yggdrasil.YggdrasilEnvironment;
import net.flashii.mcexts.URLs;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(YggdrasilEnvironment.class)
public abstract class YggdrasilEnvironmentMixin {
@Redirect(
method = "<init>",
at = @At(
value = "NEW",
target = "com/mojang/authlib/Environment",
ordinal = 0,
remap = false
),
remap = false
)
private Environment init(String sessionHost, String servicesHost, String name) {
return new Environment(URLs.getSessionHost(), servicesHost, name);
}
}

View file

@ -1,24 +1,140 @@
package net.flashii.mcexts.mixin; package net.flashii.mcexts.mixin;
import com.google.common.collect.Iterables;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.SignatureState;
import com.mojang.authlib.minecraft.client.ObjectMapper;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.yggdrasil.ProfileResult;
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse;
import com.mojang.util.UndashedUuid;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.UUID;
import net.flashii.mcexts.ProfileResultCache;
import net.flashii.mcexts.Tools;
import net.flashii.mcexts.URLs;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(YggdrasilMinecraftSessionService.class) @Mixin(YggdrasilMinecraftSessionService.class)
public abstract class YggdrasilMinecraftSessionServiceMixin { public abstract class YggdrasilMinecraftSessionServiceMixin {
@ModifyVariable(method = "getTextures(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;", at = @At("HEAD"), argsOnly = true, remap = false) private static ObjectMapper objectMapper = ObjectMapper.create();
private boolean interceptGetTexturesRequireSecure(boolean requireSecure) { private static HashMap<UUID, ProfileResultCache> profileCache = new HashMap<>();
return false;
private static ProfileResult getProfileFromMince(UUID profileId, boolean allowCache) {
if(allowCache && profileCache.containsKey(profileId)) {
ProfileResultCache cachedProfile = profileCache.get(profileId);
if(!cachedProfile.needsRefresh())
return cachedProfile.value;
Tools.Log.info("[FII MIXIN] Cache expired!!!!!!!");
} else {
Tools.Log.info("[FII MIXIN] Forcing new fetch!!!!!!!");
}
ProfileResult profileResult = null;
try {
// this should probably have caching idk at what frequency this will get called
URI url;
try {
url = new URI(String.format("%s/session/minecraft/profile/%s", URLs.getSessionHost(), UndashedUuid.toString(profileId)));
} catch(URISyntaxException ex) {
Tools.Log.info("[FII MIXIN] INCORRECTLY FORMATTED PROFILE URL!!!!!!!!!!");
return profileResult;
}
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(url).GET().build();
HttpResponse<String> response;
try {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
} catch(IOException | InterruptedException ex) {
Tools.Log.info("[FII MIXIN] FAILED TO SEND PROFILE REQUEST!!!!!!!!!!");
return profileResult;
}
if(response.statusCode() != 200) {
Tools.Log.info("[FII MIXIN] NON-200 PROFILE RESPONSE CODE!!!!!!!!!!");
return profileResult;
}
MinecraftProfilePropertiesResponse profileResponse = objectMapper.readValue(response.body(), MinecraftProfilePropertiesResponse.class);
if(profileResponse != null)
profileResult = new ProfileResult(profileResponse.toProfile());
} finally {
profileCache.put(profileId, new ProfileResultCache(profileResult));
}
return profileResult;
} }
@ModifyVariable(method = "fillProfileProperties(Lcom/mojang/authlib/GameProfile;Z)Lcom/mojang/authlib/GameProfile;", at = @At("HEAD"), argsOnly = true, remap = false) @Inject(
private boolean interceptFillProfilePropertiesRequireSecure(boolean requireSecure) { method = "getPackedTextures(Lcom/mojang/authlib/GameProfile;)Lcom/mojang/authlib/properties/Property;",
return true; at = @At("RETURN"),
cancellable = true,
remap = false
)
private void getPackedTextures(GameProfile profile, CallbackInfoReturnable<Property> cir) {
boolean replace = false;
Property prop = cir.getReturnValue();
if(prop == null) {
ProfileResult result = getProfileFromMince(profile.getId(), true);
if(result == null) {
Tools.Log.info("[FII MIXIN] PROFILE RESULT IS NULL!!!!!!!!");
} else {
replace = true;
prop = (Property)Iterables.getFirst(result.profile().getProperties().get("textures"), (Object)null);
}
}
if(replace) {
cir.setReturnValue(prop);
cir.cancel();
}
} }
@ModifyVariable(method = "fillGameProfile(Lcom/mojang/authlib/GameProfile;Z)Lcom/mojang/authlib/GameProfile;", at = @At("HEAD"), argsOnly = true, remap = false) @Inject(
private boolean interceptFillGameProfileRequireSecure(boolean requireSecure) { method = "getSecurePropertyValue(Lcom/mojang/authlib/properties/Property;)Ljava/lang/String;",
return false; at = @At("HEAD"),
cancellable = true,
remap = false
)
public void getSecurePropertyValue(Property property, CallbackInfoReturnable<String> cir) {
cir.setReturnValue(property.value());
cir.cancel();
}
@Inject(
method = "getPropertySignatureState(Lcom/mojang/authlib/properties/Property;)Lcom/mojang/authlib/SignatureState;",
at = @At("HEAD"),
cancellable = true,
remap = false
)
private void getPropertySignatureState(Property property, CallbackInfoReturnable<SignatureState> cir) {
cir.setReturnValue(SignatureState.SIGNED);
cir.cancel();
}
@Inject(
method = "fetchProfile(Ljava/util/UUID;Z)Lcom/mojang/authlib/yggdrasil/ProfileResult;",
at = @At("HEAD"),
cancellable = true,
remap = false
)
public void fetchProfile(UUID profileId, boolean requireSecure, CallbackInfoReturnable<ProfileResult> cir) {
ProfileResult result = getProfileFromMince(profileId, !requireSecure);
if(result != null) {
cir.setReturnValue(result);
cir.cancel();
}
} }
} }

View file

@ -19,7 +19,7 @@
], ],
"depends": { "depends": {
"fabricloader": ">=0.14.22", "fabricloader": ">=0.14.22",
"minecraft": "~1.20.1", "minecraft": ">=1.21",
"java": ">=17" "java": ">=21"
} }
} }

View file

@ -1,11 +1,12 @@
{ {
"required": true, "required": true,
"package": "net.flashii.mcexts.mixin", "package": "net.flashii.mcexts.mixin",
"compatibilityLevel": "JAVA_17", "compatibilityLevel": "JAVA_21",
"mixins": [ "mixins": [
"EnvironmentMixin", "PlayerListEntryMixin",
"PlayerListHudMixin", "PlayerListHudMixin",
"TextureUrlCheckerMixin", "TextureUrlCheckerMixin",
"YggdrasilEnvironmentMixin",
"YggdrasilMinecraftSessionServiceMixin" "YggdrasilMinecraftSessionServiceMixin"
], ],
"server": [ "server": [