From f730f4b36307300017c4bb5931f8a8a238028ad1 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 14 May 2026 20:49:46 +0200 Subject: [PATCH] fix(spigot): forward skin profile properties --- spigot/build.gradle.kts | 15 +++ .../connect/addon/data/SpigotDataHandler.java | 8 +- .../listener/PaperProfileListener.java | 9 +- .../listener/PaperProfileProperties.java | 46 ++++++++ .../connect/util/SpigotGameProfiles.java | 111 ++++++++++++++++++ .../addon/data/SpigotGameProfilesTest.java | 57 +++++++++ .../listener/PaperProfilePropertiesTest.java | 55 +++++++++ 7 files changed, 294 insertions(+), 7 deletions(-) create mode 100644 spigot/src/main/java/com/minekube/connect/listener/PaperProfileProperties.java create mode 100644 spigot/src/main/java/com/minekube/connect/util/SpigotGameProfiles.java create mode 100644 spigot/src/test/java/com/minekube/connect/addon/data/SpigotGameProfilesTest.java create mode 100644 spigot/src/test/java/com/minekube/connect/listener/PaperProfilePropertiesTest.java diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 5f5aa8009..df4bbd895 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -22,6 +22,21 @@ dependencies { attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17) } } + + testImplementation("org.junit.jupiter:junit-jupiter:5.10.5") + testImplementation("com.mojang", "authlib", authlibVersion) + testImplementation("dev.folia", "folia-api", Versions.spigotVersion) { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17) + } + } + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks { + test { + useJUnitPlatform() + } } relocate("com.google.inject") diff --git a/spigot/src/main/java/com/minekube/connect/addon/data/SpigotDataHandler.java b/spigot/src/main/java/com/minekube/connect/addon/data/SpigotDataHandler.java index f9bca9bad..35e43ade8 100644 --- a/spigot/src/main/java/com/minekube/connect/addon/data/SpigotDataHandler.java +++ b/spigot/src/main/java/com/minekube/connect/addon/data/SpigotDataHandler.java @@ -35,6 +35,7 @@ import com.minekube.connect.network.netty.LocalSession.Context; import com.minekube.connect.util.ClassNames; import com.minekube.connect.util.ProxyUtils; +import com.minekube.connect.util.SpigotGameProfiles; import com.mojang.authlib.GameProfile; import java.net.InetSocketAddress; import java.util.function.UnaryOperator; @@ -180,11 +181,8 @@ public boolean channelRead(Object packet) throws Exception { setValue(packetListener, ClassNames.VELOCITY_LOGIN_MESSAGE_ID, 0); } - // Set the player's correct GameProfile - GameProfile gameProfile = new GameProfile( - sessionCtx.getPlayer().getUniqueId(), - sessionCtx.getPlayer().getUsername() - ); + // Set the player's correct GameProfile, including signed texture properties for skins. + GameProfile gameProfile = SpigotGameProfiles.fromConnectProfile(sessionCtx.getPlayer().getGameProfile()); // We have to fake the offline player (login) cycle if (ClassNames.IS_PRE_1_20_2) { diff --git a/spigot/src/main/java/com/minekube/connect/listener/PaperProfileListener.java b/spigot/src/main/java/com/minekube/connect/listener/PaperProfileListener.java index 993647aa8..36166df61 100644 --- a/spigot/src/main/java/com/minekube/connect/listener/PaperProfileListener.java +++ b/spigot/src/main/java/com/minekube/connect/listener/PaperProfileListener.java @@ -29,6 +29,7 @@ import com.destroystokyo.paper.profile.ProfileProperty; import com.google.inject.Inject; import com.minekube.connect.api.SimpleConnectApi; +import com.minekube.connect.api.player.ConnectPlayer; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -41,16 +42,20 @@ public final class PaperProfileListener implements Listener { @EventHandler // TODO robin: remove or replace with session proposal player props public void onFill(PreFillProfileEvent event) { UUID id = event.getPlayerProfile().getId(); + ConnectPlayer player = id != null ? this.api.getPlayer(id) : null; // back when this event got added the PlayerProfile class didn't have the // hasProperty / hasTextures methods - if (id == null || !this.api.isConnectPlayer(id) || + if (player == null || event.getPlayerProfile().getProperties().stream().anyMatch( prop -> "textures".equals(prop.getName()))) { return; } Set properties = new HashSet<>(event.getPlayerProfile().getProperties()); - properties.add(new ProfileProperty("textures", "", "")); + properties.addAll(PaperProfileProperties.fromConnectProfile(player.getGameProfile())); + if (properties.stream().noneMatch(prop -> "textures".equals(prop.getName()))) { + properties.add(new ProfileProperty("textures", "", "")); + } event.setProperties(properties); } } diff --git a/spigot/src/main/java/com/minekube/connect/listener/PaperProfileProperties.java b/spigot/src/main/java/com/minekube/connect/listener/PaperProfileProperties.java new file mode 100644 index 000000000..6338df844 --- /dev/null +++ b/spigot/src/main/java/com/minekube/connect/listener/PaperProfileProperties.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021-2022 Minekube. https://minekube.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Minekube + * @link https://github.com/minekube/connect-java + */ + +package com.minekube.connect.listener; + +import com.destroystokyo.paper.profile.ProfileProperty; +import java.util.HashSet; +import java.util.Set; + +final class PaperProfileProperties { + private PaperProfileProperties() { + } + + static Set fromConnectProfile(com.minekube.connect.api.player.GameProfile connectProfile) { + Set properties = new HashSet<>(); + for (com.minekube.connect.api.player.GameProfile.Property property : connectProfile.getProperties()) { + String signature = property.getSignature(); + properties.add(signature == null || signature.isEmpty() + ? new ProfileProperty(property.getName(), property.getValue()) + : new ProfileProperty(property.getName(), property.getValue(), signature)); + } + return properties; + } +} diff --git a/spigot/src/main/java/com/minekube/connect/util/SpigotGameProfiles.java b/spigot/src/main/java/com/minekube/connect/util/SpigotGameProfiles.java new file mode 100644 index 000000000..01883982c --- /dev/null +++ b/spigot/src/main/java/com/minekube/connect/util/SpigotGameProfiles.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021-2022 Minekube. https://minekube.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Minekube + * @link https://github.com/minekube/connect-java + */ + +package com.minekube.connect.util; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.UUID; + +public final class SpigotGameProfiles { + private SpigotGameProfiles() { + } + + public static GameProfile fromConnectProfile(com.minekube.connect.api.player.GameProfile connectProfile) { + GameProfile profile = newGameProfile(connectProfile); + if (profile != null) { + return profile; + } + + profile = new GameProfile(connectProfile.getUniqueId(), connectProfile.getUsername()); + Object properties = properties(profile); + for (com.minekube.connect.api.player.GameProfile.Property property : connectProfile.getProperties()) { + addProperty(properties, property.getName(), authlibProperty(property)); + } + return profile; + } + + private static Property authlibProperty(com.minekube.connect.api.player.GameProfile.Property property) { + String signature = property.getSignature(); + return signature == null || signature.isEmpty() + ? new Property(property.getName(), property.getValue()) + : new Property(property.getName(), property.getValue(), signature); + } + + private static GameProfile newGameProfile(com.minekube.connect.api.player.GameProfile connectProfile) { + try { + Class propertyMapClass = Class.forName( + "com.mojang.authlib.properties.PropertyMap", false, GameProfile.class.getClassLoader()); + Constructor gameProfileConstructor = + GameProfile.class.getConstructor(UUID.class, String.class, propertyMapClass); + String guavaPackage = String.join(".", "com", "google", "common", "collect") + "."; + ClassLoader classLoader = propertyMapClass.getClassLoader(); + Class multimapClass = Class.forName(guavaPackage + "Multimap", false, classLoader); + Class hashMultimapClass = Class.forName(guavaPackage + "HashMultimap", false, classLoader); + Object multimap = hashMultimapClass.getMethod("create").invoke(null); + for (com.minekube.connect.api.player.GameProfile.Property property : connectProfile.getProperties()) { + addProperty(multimap, property.getName(), authlibProperty(property)); + } + + Object propertyMap = propertyMapClass.getConstructor(multimapClass).newInstance(multimap); + return gameProfileConstructor.newInstance( + connectProfile.getUniqueId(), connectProfile.getUsername(), propertyMap); + } catch (NoSuchMethodException ignored) { + return null; + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new IllegalStateException("Failed to create GameProfile with properties", e); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Failed to create GameProfile properties", e); + } + } + + private static Object properties(GameProfile profile) { + try { + Method properties = GameProfile.class.getMethod("properties"); + return properties.invoke(profile); + } catch (NoSuchMethodException ignored) { + try { + Method getProperties = GameProfile.class.getMethod("getProperties"); + return getProperties.invoke(profile); + } catch (Exception e) { + throw new IllegalStateException("Failed to get GameProfile properties", e); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to get GameProfile properties", e); + } + } + + private static void addProperty(Object properties, String name, Property property) { + try { + Method put = properties.getClass().getMethod("put", Object.class, Object.class); + put.invoke(properties, name, property); + } catch (Exception e) { + throw new IllegalStateException("Failed to add GameProfile property " + name, e); + } + } +} diff --git a/spigot/src/test/java/com/minekube/connect/addon/data/SpigotGameProfilesTest.java b/spigot/src/test/java/com/minekube/connect/addon/data/SpigotGameProfilesTest.java new file mode 100644 index 000000000..f7ca6e92d --- /dev/null +++ b/spigot/src/test/java/com/minekube/connect/addon/data/SpigotGameProfilesTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021-2022 Minekube. https://minekube.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Minekube + * @link https://github.com/minekube/connect-java + */ + +package com.minekube.connect.addon.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.minekube.connect.api.player.GameProfile; +import com.minekube.connect.util.SpigotGameProfiles; +import com.mojang.authlib.properties.Property; +import java.util.Collections; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class SpigotGameProfilesTest { + @Test + void copiesSignedTexturePropertyFromConnectProfile() { + UUID uuid = UUID.fromString("c66dfcbc-4bd2-4a29-8c76-eadf80faa08a"); + GameProfile connectProfile = new GameProfile( + "RoboFlax2", + uuid, + Collections.singletonList(new GameProfile.Property("textures", "skin-value", "skin-signature")) + ); + + com.mojang.authlib.GameProfile profile = SpigotGameProfiles.fromConnectProfile(connectProfile); + + Property texture = profile.getProperties().get("textures").iterator().next(); + assertEquals(uuid, profile.getId()); + assertEquals("RoboFlax2", profile.getName()); + assertEquals("skin-value", texture.getValue()); + assertEquals("skin-signature", texture.getSignature()); + assertTrue(texture.hasSignature()); + } +} diff --git a/spigot/src/test/java/com/minekube/connect/listener/PaperProfilePropertiesTest.java b/spigot/src/test/java/com/minekube/connect/listener/PaperProfilePropertiesTest.java new file mode 100644 index 000000000..61faa60ea --- /dev/null +++ b/spigot/src/test/java/com/minekube/connect/listener/PaperProfilePropertiesTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021-2022 Minekube. https://minekube.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Minekube + * @link https://github.com/minekube/connect-java + */ + +package com.minekube.connect.listener; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.destroystokyo.paper.profile.ProfileProperty; +import com.minekube.connect.api.player.GameProfile; +import java.util.Collections; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class PaperProfilePropertiesTest { + @Test + void convertsSignedTexturePropertyForPaperProfilePrefill() { + GameProfile connectProfile = new GameProfile( + "RoboFlax2", + UUID.fromString("c66dfcbc-4bd2-4a29-8c76-eadf80faa08a"), + Collections.singletonList(new GameProfile.Property("textures", "skin-value", "skin-signature")) + ); + + ProfileProperty texture = PaperProfileProperties.fromConnectProfile(connectProfile) + .iterator() + .next(); + + assertEquals("textures", texture.getName()); + assertEquals("skin-value", texture.getValue()); + assertEquals("skin-signature", texture.getSignature()); + assertTrue(texture.isSigned()); + } +}