diff --git a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java index 9723cf58..f7e4b6fd 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java +++ b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java @@ -39,6 +39,7 @@ import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.PreLoginEvent; import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventPriority; @@ -46,10 +47,10 @@ import org.geysermc.floodgate.api.ProxyFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; -import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.skin.SkinApplier; import org.geysermc.floodgate.skin.SkinDataImpl; import org.geysermc.floodgate.util.LanguageManager; +import org.geysermc.floodgate.util.MojangUtils; import org.geysermc.floodgate.util.ReflectionUtils; @SuppressWarnings("ConstantConditions") @@ -66,7 +67,7 @@ public final class BungeeListener implements Listener { checkNotNull(PLAYER_NAME, "Initial name field cannot be null"); } - @Inject private ProxyFloodgateConfig config; + @Inject private Plugin plugin; @Inject private ProxyFloodgateApi api; @Inject private LanguageManager languageManager; @Inject private FloodgateLogger logger; @@ -80,6 +81,8 @@ public final class BungeeListener implements Listener { @Named("kickMessageAttribute") private AttributeKey kickMessageAttribute; + @Inject private MojangUtils mojangUtils; + @EventHandler(priority = EventPriority.LOWEST) public void onPreLogin(PreLoginEvent event) { // well, no reason to check if the player will be kicked anyway @@ -127,13 +130,28 @@ public void onLogin(LoginEvent event) { @EventHandler(priority = EventPriority.LOWEST) public void onPostLogin(PostLoginEvent event) { - // To fix the February 2 2022 Mojang authentication changes - if (!config.isSendFloodgateData()) { - FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId()); - if (player != null && !player.isLinked()) { - skinApplier.applySkin(player, new SkinDataImpl("", "")); - } + FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId()); + + // Skin look up (on Spigot and friends) would result in it failing, so apply a default skin + if (!player.isLinked()) { + skinApplier.applySkin(player, SkinDataImpl.DEFAULT_SKIN); + return; } + + // Floodgate players are seen as offline mode players, meaning we have to look up + // the linked player's textures ourselves + + event.registerIntent(plugin); + + mojangUtils.skinFor(player.getJavaUniqueId()) + .exceptionally(exception -> { + logger.debug("Unexpected skin fetch error for " + player.getJavaUniqueId(), exception); + return SkinDataImpl.DEFAULT_SKIN; + }) + .thenAccept(skin -> { + skinApplier.applySkin(player, skin); + event.completeIntent(plugin); + }); } @EventHandler(priority = EventPriority.HIGHEST) diff --git a/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java b/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java index ce6105ec..fa2aaad2 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java +++ b/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java @@ -92,8 +92,6 @@ public void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinDat SkinData currentSkin = currentSkin(properties); SkinApplyEvent event = new SkinApplyEventImpl(floodgatePlayer, currentSkin, skinData); - event.setCancelled(floodgatePlayer.isLinked()); - eventBus.fire(event); if (event.isCancelled()) { diff --git a/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java b/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java index 9f44af79..1e2247e2 100644 --- a/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java +++ b/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java @@ -29,8 +29,14 @@ import java.util.Objects; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; +import org.geysermc.floodgate.util.Constants; public class SkinDataImpl implements SkinData { + public static final SkinData DEFAULT_SKIN = new SkinDataImpl( + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + private final String value; private final String signature; diff --git a/core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java b/core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java new file mode 100644 index 00000000..423ff44b --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * 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 GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; +import org.geysermc.floodgate.skin.SkinDataImpl; +import org.geysermc.floodgate.util.HttpClient.HttpResponse; + +@Singleton +public class MojangUtils { + private final Cache SKIN_CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .maximumSize(500) + .build(); + + @Inject private HttpClient httpClient; + @Inject + @Named("commonPool") + private ExecutorService commonPool; + + public CompletableFuture<@NonNull SkinData> skinFor(UUID playerId) { + return CompletableFuture.supplyAsync(() -> { + try { + return SKIN_CACHE.get(playerId, () -> fetchSkinFor(playerId)); + } catch (ExecutionException exception) { + throw new RuntimeException(exception.getCause()); + } + }, commonPool); + } + + private @NonNull SkinData fetchSkinFor(UUID playerId) { + HttpResponse httpResponse = httpClient.get( + String.format(Constants.PROFILE_WITH_PROPERTIES_URL, playerId.toString())); + + if (httpResponse.getHttpCode() != 200) { + return SkinDataImpl.DEFAULT_SKIN; + } + + JsonObject response = httpResponse.getResponse(); + + if (response == null) { + return SkinDataImpl.DEFAULT_SKIN; + } + + JsonArray properties = response.getAsJsonArray("properties"); + + if (properties.size() == 0) { + return SkinDataImpl.DEFAULT_SKIN; + } + + for (JsonElement property : properties) { + if (!property.isJsonObject()) { + continue; + } + + JsonObject propertyObject = property.getAsJsonObject(); + + if (!propertyObject.has("name") + || !propertyObject.has("value") + || !propertyObject.has("signature") + || !propertyObject.get("name").getAsString().equals("textures")) { + continue; + } + + return new SkinDataImpl( + propertyObject.get("value").getAsString(), + propertyObject.get("signature").getAsString() + ); + } + + return SkinDataImpl.DEFAULT_SKIN; + } +} diff --git a/core/src/main/templates/org/geysermc/floodgate/util/Constants.java b/core/src/main/templates/org/geysermc/floodgate/util/Constants.java index 62cec467..1eec715a 100644 --- a/core/src/main/templates/org/geysermc/floodgate/util/Constants.java +++ b/core/src/main/templates/org/geysermc/floodgate/util/Constants.java @@ -56,6 +56,9 @@ public final class Constants { public static final String LATEST_VERSION_URL = "https://download.geysermc.org/v2/projects/%s/versions/latest/builds/latest"; + public static final String PROFILE_WITH_PROPERTIES_URL = + "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false"; + public static final String NTP_SERVER = "time.cloudflare.com"; public static final String INTERNAL_ERROR_MESSAGE = @@ -70,4 +73,7 @@ public final class Constants { public static final int HANDSHAKE_PACKET_ID = 0; public static final int LOGIN_SUCCESS_PACKET_ID = 2; public static final int SET_COMPRESSION_PACKET_ID = 3; + + public static final String DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE = "ewogICJ0aW1lc3RhbXAiIDogMTcxNTcxNzM1NTI2MywKICAicHJvZmlsZUlkIiA6ICIyMWUzNjdkNzI1Y2Y0ZTNiYjI2OTJjNGEzMDBhNGRlYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJHZXlzZXJNQyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMWY0NzdlYjFhN2JlZWU2MzFjMmNhNjRkMDZmOGY2OGZhOTNhMzM4NmQwNDQ1MmFiMjdmNDNhY2RmMWI2MGNiIgogICAgfQogIH0KfQ"; + public static final String DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE = "dFKIZ5d6vNqCSe1IFGiVLjt3cnW8qh4qNP2umg9zqkX9bvAQawuR1iuO1kCD/+ye8A6GQFv2wRCdxdrjp5+Vrr0SsWqMnsYDN8cEg6CD18mAnaKI1TYDuGbdJaqLyGqN5wqSMdHxchs9iovFkde5ir4aYdvHkA11vOTi11L4kUzETGzJ4iKVuZOv4dq+B7wFAWqp4n8QZfhixyvemFazQHlLmxnuhU+jhpZMvYY9MAaRAJonfy/wJe9LymbTe0EJ8N+NwZQDrEUzgfBFo4OIGDqRZwvydInCqkjhPMtHCSL25VOKwcFocYpRYbk4eIKM4CLjYlBiQGki+XKsPaljwjVhnT0jUupSf7yraGb3T0CsVBjhDbIIIp9nytlbO0GvxHu0TzYjkr4Iji0do5jlCKQ/OasXcL21wd6ozw0t1QZnnzxi9ewSuyYVY9ErmWdkww1OtCIgJilceEBwNAB8+mhJ062WFaYPgJQAmOREM8InW33dbbeENMFhQi4LIO5P7p9ye3B4Lrwm20xtd9wJk3lewzcs8ezh0LUF6jPSDQDivgSKU49mLCTmOi+WZh8zKjjxfVEtNZON2W+3nct0LiWBVsQ55HzlvF0FFxuRVm6pxi6MQK2ernv3DQl0hUqyQ1+RV9nfZXTQOAUzwLjKx3t2zKqyZIiNEKLE+iAXrsE="; } diff --git a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java index 36f34e9d..383fa7e9 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java +++ b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java @@ -29,6 +29,7 @@ import static org.geysermc.floodgate.util.ReflectionUtils.setValue; import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.InvocationTargetException; @@ -38,9 +39,16 @@ import org.geysermc.floodgate.player.FloodgateHandshakeHandler; import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult; import org.geysermc.floodgate.util.ClassNames; +import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.ProxyUtils; public final class SpigotDataHandler extends CommonDataHandler { + private static final Property DEFAULT_TEXTURE_PROPERTY = new Property( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + private Object networkManager; private FloodgatePlayer player; private boolean proxyData; @@ -171,6 +179,13 @@ private boolean checkAndHandleLogin(Object packet) throws Exception { player.getCorrectUniqueId(), player.getCorrectUsername() ); + if (!player.isLinked()) { + // Otherwise game server will try to fetch the skin from Mojang. + // No need to worry that this overrides proxy data, because those won't reach this + // method / are already removed (in the case of username validation) + gameProfile.getProperties().put("textures", DEFAULT_TEXTURE_PROPERTY); + } + // we have to fake the offline player (login) cycle if (ClassNames.IS_PRE_1_20_2) { diff --git a/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java b/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java index 29ade2ec..836b9fb2 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java +++ b/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java @@ -26,20 +26,24 @@ package org.geysermc.floodgate.listener; import com.destroystokyo.paper.event.profile.PreFillProfileEvent; -import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import com.google.inject.Inject; import java.util.HashSet; import java.util.Set; import java.util.UUID; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; import org.geysermc.floodgate.api.SimpleFloodgateApi; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.util.Constants; public final class PaperProfileListener implements Listener { + private static final ProfileProperty DEFAULT_TEXTURE_PROPERTY = new ProfileProperty( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + @Inject private SimpleFloodgateApi api; @EventHandler @@ -62,26 +66,8 @@ public void onFill(PreFillProfileEvent event) { } Set properties = new HashSet<>(event.getPlayerProfile().getProperties()); - properties.add(new ProfileProperty("textures", "", "")); - event.setProperties(properties); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - Player bukkitPlayer = event.getPlayer(); - FloodgatePlayer player = api.getPlayer(bukkitPlayer.getUniqueId()); - if (player == null || player.isLinked()) { - return; - } - - PlayerProfile profile = bukkitPlayer.getPlayerProfile(); - if (profile.getProperties().stream().noneMatch( - prop -> "textures".equals(prop.getName()) && prop.getValue().isEmpty() - && prop.getSignature() != null && prop.getSignature().isEmpty())) { - return; - } + properties.add(DEFAULT_TEXTURE_PROPERTY); - profile.removeProperty("textures"); - bukkitPlayer.setPlayerProfile(profile); + event.setProperties(properties); } } diff --git a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java index 8000f520..82c7abcc 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java +++ b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java @@ -36,6 +36,7 @@ import com.google.common.cache.CacheBuilder; import com.google.inject.Inject; import com.google.inject.name.Named; +import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; @@ -48,7 +49,7 @@ import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.Field; -import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import net.kyori.adventure.text.Component; @@ -56,12 +57,16 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.ProxyFloodgateConfig; +import org.geysermc.floodgate.skin.SkinDataImpl; +import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.LanguageManager; +import org.geysermc.floodgate.util.MojangUtils; public final class VelocityListener { private static final Field INITIAL_MINECRAFT_CONNECTION; private static final Field INITIAL_CONNECTION_DELEGATE; private static final Field CHANNEL; + private static final Property DEFAULT_TEXTURE_PROPERTY; static { Class initialConnection = getPrefixedClass("connection.client.InitialInboundConnection"); @@ -82,6 +87,12 @@ public final class VelocityListener { } CHANNEL = getFieldOfType(minecraftConnection, Channel.class); + + DEFAULT_TEXTURE_PROPERTY = new Property( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); } private final Cache playerCache = @@ -103,6 +114,9 @@ public final class VelocityListener { @Named("kickMessageAttribute") private AttributeKey kickMessageAttribute; + @Inject + private MojangUtils mojangUtils; + @Subscribe(order = PostOrder.EARLY) public void onPreLogin(PreLoginEvent event) { FloodgatePlayer player = null; @@ -139,22 +153,38 @@ public void onPreLogin(PreLoginEvent event) { } @Subscribe(order = PostOrder.EARLY) - public void onGameProfileRequest(GameProfileRequestEvent event) { + public void onGameProfileRequest(GameProfileRequestEvent event, Continuation continuation) { FloodgatePlayer player = playerCache.getIfPresent(event.getConnection()); - if (player != null) { - playerCache.invalidate(event.getConnection()); + if (player == null) { + return; + } + playerCache.invalidate(event.getConnection()); - GameProfile profile = new GameProfile( + // Skin look up (on Spigot and friends) would result in it failing, so apply a default skin + if (!player.isLinked()) { + event.setGameProfile(new GameProfile( player.getCorrectUniqueId(), player.getCorrectUsername(), - Collections.emptyList() - ); - // The texture properties addition is to fix the February 2 2022 Mojang authentication changes - if (!config.isSendFloodgateData() && !player.isLinked()) { - profile = profile.addProperty(new Property("textures", "", "")); - } - event.setGameProfile(profile); + List.of(DEFAULT_TEXTURE_PROPERTY) + )); + return; } + + // Floodgate players are seen as offline mode players, meaning we have to look up + // the linked player's textures ourselves + + mojangUtils.skinFor(player.getJavaUniqueId()) + .exceptionally(exception -> { + logger.debug("Unexpected skin fetch error for " + player.getJavaUniqueId(), exception); + return SkinDataImpl.DEFAULT_SKIN; + }).thenAccept(skin -> { + event.setGameProfile(new GameProfile( + player.getCorrectUniqueId(), + player.getCorrectUsername(), + List.of(new Property("textures", skin.value(), skin.signature())) + )); + continuation.resume(); + }); } @Subscribe(order = PostOrder.LAST)