Skip to content

Commit b898894

Browse files
authored
Fix Spigot/Paper skin forwarding for Connect tunnels
Merge PR #27: preserve signed skin profile properties for direct Spigot/Paper Connect tunnels across old and new authlib versions.
1 parent 7df3100 commit b898894

7 files changed

Lines changed: 294 additions & 7 deletions

File tree

spigot/build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ dependencies {
2222
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
2323
}
2424
}
25+
26+
testImplementation("org.junit.jupiter:junit-jupiter:5.10.5")
27+
testImplementation("com.mojang", "authlib", authlibVersion)
28+
testImplementation("dev.folia", "folia-api", Versions.spigotVersion) {
29+
attributes {
30+
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
31+
}
32+
}
33+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
34+
}
35+
36+
tasks {
37+
test {
38+
useJUnitPlatform()
39+
}
2540
}
2641

2742
relocate("com.google.inject")

spigot/src/main/java/com/minekube/connect/addon/data/SpigotDataHandler.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.minekube.connect.network.netty.LocalSession.Context;
3636
import com.minekube.connect.util.ClassNames;
3737
import com.minekube.connect.util.ProxyUtils;
38+
import com.minekube.connect.util.SpigotGameProfiles;
3839
import com.mojang.authlib.GameProfile;
3940
import java.net.InetSocketAddress;
4041
import java.util.function.UnaryOperator;
@@ -180,11 +181,8 @@ public boolean channelRead(Object packet) throws Exception {
180181
setValue(packetListener, ClassNames.VELOCITY_LOGIN_MESSAGE_ID, 0);
181182
}
182183

183-
// Set the player's correct GameProfile
184-
GameProfile gameProfile = new GameProfile(
185-
sessionCtx.getPlayer().getUniqueId(),
186-
sessionCtx.getPlayer().getUsername()
187-
);
184+
// Set the player's correct GameProfile, including signed texture properties for skins.
185+
GameProfile gameProfile = SpigotGameProfiles.fromConnectProfile(sessionCtx.getPlayer().getGameProfile());
188186

189187
// We have to fake the offline player (login) cycle
190188
if (ClassNames.IS_PRE_1_20_2) {

spigot/src/main/java/com/minekube/connect/listener/PaperProfileListener.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.destroystokyo.paper.profile.ProfileProperty;
3030
import com.google.inject.Inject;
3131
import com.minekube.connect.api.SimpleConnectApi;
32+
import com.minekube.connect.api.player.ConnectPlayer;
3233
import java.util.HashSet;
3334
import java.util.Set;
3435
import java.util.UUID;
@@ -41,16 +42,20 @@ public final class PaperProfileListener implements Listener {
4142
@EventHandler // TODO robin: remove or replace with session proposal player props
4243
public void onFill(PreFillProfileEvent event) {
4344
UUID id = event.getPlayerProfile().getId();
45+
ConnectPlayer player = id != null ? this.api.getPlayer(id) : null;
4446
// back when this event got added the PlayerProfile class didn't have the
4547
// hasProperty / hasTextures methods
46-
if (id == null || !this.api.isConnectPlayer(id) ||
48+
if (player == null ||
4749
event.getPlayerProfile().getProperties().stream().anyMatch(
4850
prop -> "textures".equals(prop.getName()))) {
4951
return;
5052
}
5153

5254
Set<ProfileProperty> properties = new HashSet<>(event.getPlayerProfile().getProperties());
53-
properties.add(new ProfileProperty("textures", "", ""));
55+
properties.addAll(PaperProfileProperties.fromConnectProfile(player.getGameProfile()));
56+
if (properties.stream().noneMatch(prop -> "textures".equals(prop.getName()))) {
57+
properties.add(new ProfileProperty("textures", "", ""));
58+
}
5459
event.setProperties(properties);
5560
}
5661
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2021-2022 Minekube. https://minekube.com
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*
22+
* @author Minekube
23+
* @link https://github.com/minekube/connect-java
24+
*/
25+
26+
package com.minekube.connect.listener;
27+
28+
import com.destroystokyo.paper.profile.ProfileProperty;
29+
import java.util.HashSet;
30+
import java.util.Set;
31+
32+
final class PaperProfileProperties {
33+
private PaperProfileProperties() {
34+
}
35+
36+
static Set<ProfileProperty> fromConnectProfile(com.minekube.connect.api.player.GameProfile connectProfile) {
37+
Set<ProfileProperty> properties = new HashSet<>();
38+
for (com.minekube.connect.api.player.GameProfile.Property property : connectProfile.getProperties()) {
39+
String signature = property.getSignature();
40+
properties.add(signature == null || signature.isEmpty()
41+
? new ProfileProperty(property.getName(), property.getValue())
42+
: new ProfileProperty(property.getName(), property.getValue(), signature));
43+
}
44+
return properties;
45+
}
46+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright (c) 2021-2022 Minekube. https://minekube.com
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*
22+
* @author Minekube
23+
* @link https://github.com/minekube/connect-java
24+
*/
25+
26+
package com.minekube.connect.util;
27+
28+
import com.mojang.authlib.GameProfile;
29+
import com.mojang.authlib.properties.Property;
30+
import java.lang.reflect.Constructor;
31+
import java.lang.reflect.InvocationTargetException;
32+
import java.lang.reflect.Method;
33+
import java.util.UUID;
34+
35+
public final class SpigotGameProfiles {
36+
private SpigotGameProfiles() {
37+
}
38+
39+
public static GameProfile fromConnectProfile(com.minekube.connect.api.player.GameProfile connectProfile) {
40+
GameProfile profile = newGameProfile(connectProfile);
41+
if (profile != null) {
42+
return profile;
43+
}
44+
45+
profile = new GameProfile(connectProfile.getUniqueId(), connectProfile.getUsername());
46+
Object properties = properties(profile);
47+
for (com.minekube.connect.api.player.GameProfile.Property property : connectProfile.getProperties()) {
48+
addProperty(properties, property.getName(), authlibProperty(property));
49+
}
50+
return profile;
51+
}
52+
53+
private static Property authlibProperty(com.minekube.connect.api.player.GameProfile.Property property) {
54+
String signature = property.getSignature();
55+
return signature == null || signature.isEmpty()
56+
? new Property(property.getName(), property.getValue())
57+
: new Property(property.getName(), property.getValue(), signature);
58+
}
59+
60+
private static GameProfile newGameProfile(com.minekube.connect.api.player.GameProfile connectProfile) {
61+
try {
62+
Class<?> propertyMapClass = Class.forName(
63+
"com.mojang.authlib.properties.PropertyMap", false, GameProfile.class.getClassLoader());
64+
Constructor<GameProfile> gameProfileConstructor =
65+
GameProfile.class.getConstructor(UUID.class, String.class, propertyMapClass);
66+
String guavaPackage = String.join(".", "com", "google", "common", "collect") + ".";
67+
ClassLoader classLoader = propertyMapClass.getClassLoader();
68+
Class<?> multimapClass = Class.forName(guavaPackage + "Multimap", false, classLoader);
69+
Class<?> hashMultimapClass = Class.forName(guavaPackage + "HashMultimap", false, classLoader);
70+
Object multimap = hashMultimapClass.getMethod("create").invoke(null);
71+
for (com.minekube.connect.api.player.GameProfile.Property property : connectProfile.getProperties()) {
72+
addProperty(multimap, property.getName(), authlibProperty(property));
73+
}
74+
75+
Object propertyMap = propertyMapClass.getConstructor(multimapClass).newInstance(multimap);
76+
return gameProfileConstructor.newInstance(
77+
connectProfile.getUniqueId(), connectProfile.getUsername(), propertyMap);
78+
} catch (NoSuchMethodException ignored) {
79+
return null;
80+
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
81+
throw new IllegalStateException("Failed to create GameProfile with properties", e);
82+
} catch (ClassNotFoundException e) {
83+
throw new IllegalStateException("Failed to create GameProfile properties", e);
84+
}
85+
}
86+
87+
private static Object properties(GameProfile profile) {
88+
try {
89+
Method properties = GameProfile.class.getMethod("properties");
90+
return properties.invoke(profile);
91+
} catch (NoSuchMethodException ignored) {
92+
try {
93+
Method getProperties = GameProfile.class.getMethod("getProperties");
94+
return getProperties.invoke(profile);
95+
} catch (Exception e) {
96+
throw new IllegalStateException("Failed to get GameProfile properties", e);
97+
}
98+
} catch (Exception e) {
99+
throw new IllegalStateException("Failed to get GameProfile properties", e);
100+
}
101+
}
102+
103+
private static void addProperty(Object properties, String name, Property property) {
104+
try {
105+
Method put = properties.getClass().getMethod("put", Object.class, Object.class);
106+
put.invoke(properties, name, property);
107+
} catch (Exception e) {
108+
throw new IllegalStateException("Failed to add GameProfile property " + name, e);
109+
}
110+
}
111+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2021-2022 Minekube. https://minekube.com
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*
22+
* @author Minekube
23+
* @link https://github.com/minekube/connect-java
24+
*/
25+
26+
package com.minekube.connect.addon.data;
27+
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
import com.minekube.connect.api.player.GameProfile;
32+
import com.minekube.connect.util.SpigotGameProfiles;
33+
import com.mojang.authlib.properties.Property;
34+
import java.util.Collections;
35+
import java.util.UUID;
36+
import org.junit.jupiter.api.Test;
37+
38+
class SpigotGameProfilesTest {
39+
@Test
40+
void copiesSignedTexturePropertyFromConnectProfile() {
41+
UUID uuid = UUID.fromString("c66dfcbc-4bd2-4a29-8c76-eadf80faa08a");
42+
GameProfile connectProfile = new GameProfile(
43+
"RoboFlax2",
44+
uuid,
45+
Collections.singletonList(new GameProfile.Property("textures", "skin-value", "skin-signature"))
46+
);
47+
48+
com.mojang.authlib.GameProfile profile = SpigotGameProfiles.fromConnectProfile(connectProfile);
49+
50+
Property texture = profile.getProperties().get("textures").iterator().next();
51+
assertEquals(uuid, profile.getId());
52+
assertEquals("RoboFlax2", profile.getName());
53+
assertEquals("skin-value", texture.getValue());
54+
assertEquals("skin-signature", texture.getSignature());
55+
assertTrue(texture.hasSignature());
56+
}
57+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2021-2022 Minekube. https://minekube.com
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*
22+
* @author Minekube
23+
* @link https://github.com/minekube/connect-java
24+
*/
25+
26+
package com.minekube.connect.listener;
27+
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
import com.destroystokyo.paper.profile.ProfileProperty;
32+
import com.minekube.connect.api.player.GameProfile;
33+
import java.util.Collections;
34+
import java.util.UUID;
35+
import org.junit.jupiter.api.Test;
36+
37+
class PaperProfilePropertiesTest {
38+
@Test
39+
void convertsSignedTexturePropertyForPaperProfilePrefill() {
40+
GameProfile connectProfile = new GameProfile(
41+
"RoboFlax2",
42+
UUID.fromString("c66dfcbc-4bd2-4a29-8c76-eadf80faa08a"),
43+
Collections.singletonList(new GameProfile.Property("textures", "skin-value", "skin-signature"))
44+
);
45+
46+
ProfileProperty texture = PaperProfileProperties.fromConnectProfile(connectProfile)
47+
.iterator()
48+
.next();
49+
50+
assertEquals("textures", texture.getName());
51+
assertEquals("skin-value", texture.getValue());
52+
assertEquals("skin-signature", texture.getSignature());
53+
assertTrue(texture.isSigned());
54+
}
55+
}

0 commit comments

Comments
 (0)