From fb3e9569c53126e88f0ec785db32c970ac174874 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 2 Sep 2025 12:19:50 -0400 Subject: [PATCH 1/2] Add FetchMissingTexturesByPlayerName --- .../authlibinjector/AuthlibInjector.java | 14 +- .../transform/ClassTransformer.java | 6 +- .../FetchMissingTexturesByPlayerName.java | 140 ++++++++++++++++++ 3 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index 61703cc..4b10b89 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -61,6 +61,7 @@ import moe.yushi.authlibinjector.transform.support.BungeeCordProfileKeyTransform import moe.yushi.authlibinjector.transform.support.CitizensTransformer; import moe.yushi.authlibinjector.transform.support.ConcatenateURLTransformUnit; import moe.yushi.authlibinjector.transform.support.ConstantURLTransformUnit; +import moe.yushi.authlibinjector.transform.support.FetchMissingTexturesByPlayerName; import moe.yushi.authlibinjector.transform.support.MC52974Workaround; import moe.yushi.authlibinjector.transform.support.MC52974_1710Workaround; import moe.yushi.authlibinjector.transform.support.MainArgumentsTransformer; @@ -223,7 +224,7 @@ public final class AuthlibInjector { return a.equals(b); } - private static List createFilters(APIMetadata config) { + private static List createFilters(APIMetadata config, YggdrasilClient customClient, YggdrasilClient mojangClient) { if (Config.httpdDisabled) { log(INFO, "Disabled local HTTP server"); return emptyList(); @@ -231,9 +232,6 @@ public final class AuthlibInjector { List filters = new ArrayList<>(); - YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config)); - YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider(), Config.mojangProxy); - boolean legacySkinPolyfillDefault = !Boolean.TRUE.equals(config.getMeta().get("feature.legacy_skin_api")); if (Config.legacySkinPolyfill.isEnabled(legacySkinPolyfillDefault)) { filters.add(new LegacySkinAPIFilter(customClient)); @@ -265,7 +263,10 @@ public final class AuthlibInjector { } private static ClassTransformer createTransformer(APIMetadata config) { - URLProcessor urlProcessor = new URLProcessor(createFilters(config), new DefaultURLRedirector(config)); + YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config)); + YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider(), Config.mojangProxy); + + URLProcessor urlProcessor = new URLProcessor(createFilters(config, customClient, mojangClient), new DefaultURLRedirector(config)); ClassTransformer transformer = new ClassTransformer(); transformer.setIgnores(Config.ignoredPackages); @@ -283,6 +284,9 @@ public final class AuthlibInjector { transformer.units.add(new CitizensTransformer()); transformer.units.add(new ConcatenateURLTransformUnit()); + FetchMissingTexturesByPlayerName.setYggdrasilClient(customClient); + transformer.units.add(new FetchMissingTexturesByPlayerName()); + boolean usernameCheckDefault = Boolean.TRUE.equals(config.getMeta().get("feature.username_check")); if (Config.usernameCheck.isEnabled(usernameCheckDefault)) { log(INFO, "Username check is enforced"); diff --git a/src/main/java/moe/yushi/authlibinjector/transform/ClassTransformer.java b/src/main/java/moe/yushi/authlibinjector/transform/ClassTransformer.java index 28f3827..5d3f794 100644 --- a/src/main/java/moe/yushi/authlibinjector/transform/ClassTransformer.java +++ b/src/main/java/moe/yushi/authlibinjector/transform/ClassTransformer.java @@ -137,7 +137,7 @@ public class ClassTransformer implements ClassFileTransformer { public void accept(TransformUnit... units) { long t0 = System.nanoTime(); - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); TransformContextImpl[] ctxs = new TransformContextImpl[units.length]; ClassVisitor chain = writer; @@ -204,7 +204,7 @@ public class ClassTransformer implements ClassFileTransformer { } ClassReader reader = getClassReader(); - ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); ClassVisitor visitor = new ClassVisitor(ASM9, writer) { @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { @@ -218,7 +218,7 @@ public class ClassTransformer implements ClassFileTransformer { private void injectGeneratedMethods() { ClassReader reader = getClassReader(); - ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); ClassVisitor visitor = new ClassVisitor(ASM9, writer) { @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { diff --git a/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java b/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java new file mode 100644 index 0000000..18efeab --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java @@ -0,0 +1,140 @@ +package moe.yushi.authlibinjector.transform.support; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.lang.reflect.Constructor; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.UUID; +import java.io.UncheckedIOException; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +import static org.objectweb.asm.Opcodes.*; + +import static moe.yushi.authlibinjector.util.Logging.Level.INFO; +import static moe.yushi.authlibinjector.util.Logging.log; + +import moe.yushi.authlibinjector.transform.CallbackMethod; +import moe.yushi.authlibinjector.transform.TransformContext; +import moe.yushi.authlibinjector.transform.TransformUnit; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; +import moe.yushi.authlibinjector.yggdrasil.GameProfile; +import moe.yushi.authlibinjector.yggdrasil.GameProfile.PropertyValue; + +public class FetchMissingTexturesByPlayerName implements TransformUnit { + private static volatile YggdrasilClient yggdrasilClient; + public static void setYggdrasilClient(YggdrasilClient yggdrasilClient) { + FetchMissingTexturesByPlayerName.yggdrasilClient = yggdrasilClient; + } + + private static final ConcurrentHashMap> nameToUUIDCache = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> uuidToTexturesCache = new ConcurrentHashMap<>(); + + @CallbackMethod + public static Object getPackedTextures(Object instance, Object profile) { + try { + try { + Class gameProfileClass = profile.getClass(); + + Method getProperties = gameProfileClass.getMethod("getProperties"); + Object propertiesMap = getProperties.invoke(profile); + + Method containsKey = propertiesMap.getClass().getMethod("containsKey", Object.class); + boolean hasTextures = (boolean) containsKey.invoke(propertiesMap, "textures"); + + if (!hasTextures) { + Method getName = gameProfileClass.getMethod("getName"); + String name = (String) getName.invoke(profile); + + Optional maybeUUID = nameToUUIDCache.computeIfAbsent(name, n -> { + try { + return yggdrasilClient.queryUUID(n); + } catch (UncheckedIOException e) { + return null; + } + }); + if (maybeUUID != null && maybeUUID.isPresent()) { + UUID uuid = maybeUUID.get(); + Optional maybeTextures = uuidToTexturesCache.computeIfAbsent(uuid, u -> { + Optional maybeFullProfile; + try { + maybeFullProfile = yggdrasilClient.queryProfile(u, true); + } catch (UncheckedIOException e) { + return null; + } + return maybeFullProfile.map(fullProfile -> { + return fullProfile.properties.get("textures"); + }); + }); + if (maybeTextures != null && maybeTextures.isPresent()) { + PropertyValue textures = maybeTextures.get(); + Class propertyClass = profile.getClass() + .getClassLoader() + .loadClass("com.mojang.authlib.properties.Property"); + Constructor propertyCtor = propertyClass.getConstructor(String.class, String.class, String.class); + return propertyCtor.newInstance("textures", textures.value, textures.signature); + } + } + } + } catch (Throwable e) { + e.printStackTrace(); + } + + Method m = instance.getClass().getDeclaredMethod("getPackedTextures$original", profile.getClass()); + m.setAccessible(true); + return m.invoke(instance, profile); + } catch (Throwable e) { + e.printStackTrace(); + return null; + } + } + + @Override + public Optional transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) { + if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) { + return Optional.of(new ClassVisitor(ASM9, writer) { + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + if ("getPackedTextures".equals(name) && + "(Lcom/mojang/authlib/GameProfile;)Lcom/mojang/authlib/properties/Property;".equals(desc)) { + + ctx.markModified(); + + MethodVisitor originalMethodVisitor = super.visitMethod(access, name + "$original", desc, signature, exceptions); + + MethodVisitor hookedMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions); + if (hookedMethodVisitor != null) { + hookedMethodVisitor.visitCode(); + + // Load `this` + hookedMethodVisitor.visitVarInsn(ALOAD, 0); + // Load `profile` + hookedMethodVisitor.visitVarInsn(ALOAD, 1); + hookedMethodVisitor.visitMethodInsn(INVOKESTATIC, + "moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName", + "getPackedTextures", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + false); + + hookedMethodVisitor.visitTypeInsn(CHECKCAST, "com/mojang/authlib/properties/Property"); + hookedMethodVisitor.visitInsn(ARETURN); + hookedMethodVisitor.visitEnd(); + } + + return originalMethodVisitor; + } + return super.visitMethod(access, name, desc, signature, exceptions); + } + }); + } + return Optional.empty(); + } + + @Override + public String toString() { + return "FetchMissingTexturesByPlayerName"; + } +} From e745e87736e6bd7622c521997711079a51eb0a36 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 2 Sep 2025 14:39:38 -0400 Subject: [PATCH 2/2] FetchMissingTexturesByPlayerName old version support --- .../FetchMissingTexturesByPlayerName.java | 172 +++++++++++++----- 1 file changed, 124 insertions(+), 48 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java b/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java index 18efeab..60db666 100644 --- a/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java +++ b/src/main/java/moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName.java @@ -1,27 +1,29 @@ package moe.yushi.authlibinjector.transform.support; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Method; -import java.lang.reflect.Constructor; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.UUID; import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import static org.objectweb.asm.Opcodes.*; +import static moe.yushi.authlibinjector.util.Logging.Level.ERROR; import static moe.yushi.authlibinjector.util.Logging.Level.INFO; import static moe.yushi.authlibinjector.util.Logging.log; import moe.yushi.authlibinjector.transform.CallbackMethod; import moe.yushi.authlibinjector.transform.TransformContext; import moe.yushi.authlibinjector.transform.TransformUnit; -import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; -import moe.yushi.authlibinjector.yggdrasil.GameProfile; import moe.yushi.authlibinjector.yggdrasil.GameProfile.PropertyValue; +import moe.yushi.authlibinjector.yggdrasil.GameProfile; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; public class FetchMissingTexturesByPlayerName implements TransformUnit { private static volatile YggdrasilClient yggdrasilClient; @@ -32,60 +34,103 @@ public class FetchMissingTexturesByPlayerName implements TransformUnit { private static final ConcurrentHashMap> nameToUUIDCache = new ConcurrentHashMap<>(); private static final ConcurrentHashMap> uuidToTexturesCache = new ConcurrentHashMap<>(); - @CallbackMethod - public static Object getPackedTextures(Object instance, Object profile) { + public static Object getMissingTexturesProperty(Object profile) { + // (com.mojang.authlib.GameProfile) -> com.mojang.authlib.properties.Property + // Fetches missing textures for a GameProfile by player name via the yggdrasilClient. try { - try { + // If the GameProfile already has textures, return null. + Class gameProfileClass = profile.getClass(); + Method getProperties = gameProfileClass.getMethod("getProperties"); + Object propertiesMap = getProperties.invoke(profile); + Method containsKey = propertiesMap.getClass().getMethod("containsKey", Object.class); + boolean hasTextures = (boolean) containsKey.invoke(propertiesMap, "textures"); + if (hasTextures) { + return null; + } + + Method getName = gameProfileClass.getMethod("getName"); + String name = (String) getName.invoke(profile); + Optional maybeUUID = nameToUUIDCache.computeIfAbsent(name, n -> { + try { + return yggdrasilClient.queryUUID(n); + } catch (UncheckedIOException e) { + return null; + } + }); + if (maybeUUID == null || !maybeUUID.isPresent()) { + return null; + } + UUID uuid = maybeUUID.get(); + + Optional maybeTextures = uuidToTexturesCache.computeIfAbsent(uuid, u -> { + Optional maybeFullProfile; + try { + maybeFullProfile = yggdrasilClient.queryProfile(u, true); + } catch (UncheckedIOException e) { + return null; + } + return maybeFullProfile.map(fullProfile -> { + return fullProfile.properties.get("textures"); + }); + }); + if (maybeTextures == null || !maybeTextures.isPresent()) { + return null; + } + PropertyValue textures = maybeTextures.get(); + + log(INFO, "Successfully fetched missing textures for player " + name); + Class propertyClass = profile.getClass() + .getClassLoader() + .loadClass("com.mojang.authlib.properties.Property"); + Constructor propertyConstructor = propertyClass.getConstructor(String.class, String.class, String.class); + return propertyConstructor.newInstance("textures", textures.value, textures.signature); + } catch (Throwable e) { + e.printStackTrace(); + return null; + } + } + + @CallbackMethod + public static Object getTextures(Object instance, Object profile, boolean requireSecure) { + try { + Object property = getMissingTexturesProperty(profile); + if (property != null) { + // Fill in the existing GameProfile with the missing textures Class gameProfileClass = profile.getClass(); Method getProperties = gameProfileClass.getMethod("getProperties"); Object propertiesMap = getProperties.invoke(profile); - Method containsKey = propertiesMap.getClass().getMethod("containsKey", Object.class); - boolean hasTextures = (boolean) containsKey.invoke(propertiesMap, "textures"); + Class propertiesMapClass = propertiesMap.getClass(); + Method removeAll = propertiesMapClass.getMethod("removeAll", Object.class); + removeAll.invoke(propertiesMap, "textures"); + Method put = propertiesMapClass.getMethod("put", Object.class, Object.class); + put.invoke(propertiesMap, "textures", property); + } - if (!hasTextures) { - Method getName = gameProfileClass.getMethod("getName"); - String name = (String) getName.invoke(profile); + Method m = instance.getClass().getDeclaredMethod("getTextures$original", profile.getClass(), boolean.class); + m.setAccessible(true); + return m.invoke(instance, profile, requireSecure); + } catch (Throwable e) { + log(ERROR, "Error fetching missing textures:"); + e.printStackTrace(); + return null; + } + } - Optional maybeUUID = nameToUUIDCache.computeIfAbsent(name, n -> { - try { - return yggdrasilClient.queryUUID(n); - } catch (UncheckedIOException e) { - return null; - } - }); - if (maybeUUID != null && maybeUUID.isPresent()) { - UUID uuid = maybeUUID.get(); - Optional maybeTextures = uuidToTexturesCache.computeIfAbsent(uuid, u -> { - Optional maybeFullProfile; - try { - maybeFullProfile = yggdrasilClient.queryProfile(u, true); - } catch (UncheckedIOException e) { - return null; - } - return maybeFullProfile.map(fullProfile -> { - return fullProfile.properties.get("textures"); - }); - }); - if (maybeTextures != null && maybeTextures.isPresent()) { - PropertyValue textures = maybeTextures.get(); - Class propertyClass = profile.getClass() - .getClassLoader() - .loadClass("com.mojang.authlib.properties.Property"); - Constructor propertyCtor = propertyClass.getConstructor(String.class, String.class, String.class); - return propertyCtor.newInstance("textures", textures.value, textures.signature); - } - } - } - } catch (Throwable e) { - e.printStackTrace(); + @CallbackMethod + public static Object getPackedTextures(Object instance, Object profile) { + try { + Object property = getMissingTexturesProperty(profile); + if (property != null) { + return property; } Method m = instance.getClass().getDeclaredMethod("getPackedTextures$original", profile.getClass()); m.setAccessible(true); return m.invoke(instance, profile); } catch (Throwable e) { + log(ERROR, "Error fetching missing textures:"); e.printStackTrace(); return null; } @@ -121,11 +166,42 @@ public class FetchMissingTexturesByPlayerName implements TransformUnit { hookedMethodVisitor.visitTypeInsn(CHECKCAST, "com/mojang/authlib/properties/Property"); hookedMethodVisitor.visitInsn(ARETURN); + hookedMethodVisitor.visitMaxs(2, 2); + hookedMethodVisitor.visitEnd(); + } + + return originalMethodVisitor; + } else if ("getTextures".equals(name) && + "(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;".equals(desc)) { + ctx.markModified(); + + MethodVisitor originalMethodVisitor = super.visitMethod(access, name + "$original", desc, signature, exceptions); + MethodVisitor hookedMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions); + + if (hookedMethodVisitor != null) { + hookedMethodVisitor.visitCode(); + + // Load `this` + hookedMethodVisitor.visitVarInsn(ALOAD, 0); + // Load `profile` + hookedMethodVisitor.visitVarInsn(ALOAD, 1); + // Load `requireSecure` + hookedMethodVisitor.visitVarInsn(ILOAD, 2); + hookedMethodVisitor.visitMethodInsn(INVOKESTATIC, + "moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName", + "getTextures", + "(Ljava/lang/Object;Ljava/lang/Object;Z)Ljava/lang/Object;", + false); + + hookedMethodVisitor.visitTypeInsn(CHECKCAST, "java/util/Map"); + hookedMethodVisitor.visitInsn(ARETURN); + hookedMethodVisitor.visitMaxs(3, 3); hookedMethodVisitor.visitEnd(); } return originalMethodVisitor; } + return super.visitMethod(access, name, desc, signature, exceptions); } });