diff --git a/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java b/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java index a51252a3..708df16f 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java +++ b/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java @@ -25,10 +25,10 @@ package org.geysermc.floodgate.api; -import java.util.Collection; +import java.net.InetSocketAddress;import java.util.Collection; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import org.geysermc.cumulus.form.Form; +import org.checkerframework.checker.nullness.qual.Nullable;import org.geysermc.cumulus.form.Form; import org.geysermc.cumulus.form.util.FormBuilder; import org.geysermc.floodgate.api.event.FloodgateEventBus; import org.geysermc.floodgate.api.link.PlayerLink; @@ -59,6 +59,26 @@ static FloodgateApi getInstance() { */ int getPlayerCount(); + /** + * Get a pending FloodgatePlayer by their connection address. + * This is useful in PreLoginEvent where UUID is not yet available. + * + * @param address the InetSocketAddress of the connection + * @return FloodgatePlayer if this is a pending Bedrock connection, null otherwise + */ + @Nullable + FloodgatePlayer getPendingPlayer(InetSocketAddress address); + + /** + * Get a pending FloodgatePlayer by their raw username (without prefix). + * This is useful in PreLoginEvent where the username hasn't been modified yet. + * + * @param rawUsername the raw username without Floodgate prefix + * @return FloodgatePlayer if this is a pending Bedrock connection, null otherwise + */ + @Nullable + FloodgatePlayer getPendingPlayerByUsername(String rawUsername); + /** * Method to determine if the given online player is a bedrock player * 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 e5100da5..cdbd2ca6 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java +++ b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java @@ -32,6 +32,8 @@ import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.UUID; import net.md_5.bungee.api.connection.PendingConnection; import net.md_5.bungee.api.event.LoginEvent; @@ -47,6 +49,7 @@ import org.geysermc.floodgate.api.ProxyFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.player.PendingPlayerManager; import org.geysermc.floodgate.skin.SkinApplier; import org.geysermc.floodgate.skin.SkinDataImpl; import org.geysermc.floodgate.util.LanguageManager; @@ -82,6 +85,7 @@ public final class BungeeListener implements Listener { private AttributeKey kickMessageAttribute; @Inject private MojangUtils mojangUtils; + @Inject private PendingPlayerManager pendingPlayerManager; @EventHandler(priority = EventPriority.LOWEST) public void onPreLogin(PreLoginEvent event) { @@ -130,6 +134,13 @@ public void onLogin(LoginEvent event) { @EventHandler(priority = EventPriority.LOWEST) public void onPostLogin(PostLoginEvent event) { + SocketAddress socketAddress = event.getPlayer().getSocketAddress(); + + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + pendingPlayerManager.remove(inetSocketAddress); + } + FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId()); if (player == null) { return; @@ -160,5 +171,12 @@ public void onPostLogin(PostLoginEvent event) { @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerDisconnect(PlayerDisconnectEvent event) { api.playerRemoved(event.getPlayer().getUniqueId()); + + SocketAddress socketAddress = event.getPlayer().getSocketAddress(); + + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + pendingPlayerManager.remove(inetSocketAddress); + } } } diff --git a/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java b/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java index baf05da8..b8291646 100644 --- a/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java +++ b/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java @@ -31,18 +31,21 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.inject.Inject; +import java.net.InetSocketAddress; import java.util.Collection; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.cumulus.form.Form; import org.geysermc.cumulus.form.util.FormBuilder; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.api.unsafe.Unsafe; import org.geysermc.floodgate.config.FloodgateConfig; +import org.geysermc.floodgate.player.PendingPlayerManager; import org.geysermc.floodgate.pluginmessage.PluginMessageManager; import org.geysermc.floodgate.pluginmessage.channel.FormChannel; import org.geysermc.floodgate.pluginmessage.channel.TransferChannel; @@ -61,6 +64,7 @@ public class SimpleFloodgateApi implements FloodgateApi { @Inject private FloodgateConfig config; @Inject private HttpClient httpClient; @Inject private FloodgateLogger logger; + @Inject private PendingPlayerManager pendingPlayerManager; @Override public String getPlayerPrefix() { @@ -225,4 +229,16 @@ private FloodgatePlayer getPendingRemovePlayer(UUID correctUuid) { } return null; } + + @Override + @Nullable + public FloodgatePlayer getPendingPlayer(InetSocketAddress address) { + return pendingPlayerManager.get(address); + } + + @Override + @Nullable + public FloodgatePlayer getPendingPlayerByUsername(String rawUsername) { + return pendingPlayerManager.getByUsername(rawUsername); + } } diff --git a/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java b/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java index cddb93f7..2485bf19 100644 --- a/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java +++ b/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java @@ -67,6 +67,7 @@ import org.geysermc.floodgate.link.PlayerLinkHolder; import org.geysermc.floodgate.packet.PacketHandlersImpl; import org.geysermc.floodgate.player.FloodgateHandshakeHandler; +import org.geysermc.floodgate.player.PendingPlayerManager; import org.geysermc.floodgate.pluginmessage.PluginMessageManager; import org.geysermc.floodgate.skin.SkinUploadManager; import org.geysermc.floodgate.util.Constants; @@ -175,10 +176,11 @@ public FloodgateHandshakeHandler handshakeHandler( SkinUploadManager skinUploadManager, @Named("playerAttribute") AttributeKey playerAttribute, FloodgateLogger logger, - LanguageManager languageManager) { + LanguageManager languageManager, + PendingPlayerManager pendingPlayerManager) { return new FloodgateHandshakeHandler(handshakeHandlers, api, cipher, config, - skinUploadManager, playerAttribute, logger, languageManager); + skinUploadManager, playerAttribute, logger, languageManager, pendingPlayerManager); } @Provides diff --git a/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java b/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java index b84ff1ba..770c6ac9 100644 --- a/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java +++ b/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java @@ -67,6 +67,7 @@ public final class FloodgateHandshakeHandler { private final AttributeKey playerAttribute; private final FloodgateLogger logger; private final LanguageManager languageManager; + private final PendingPlayerManager pendingPlayerManager; public FloodgateHandshakeHandler( HandshakeHandlersImpl handshakeHandlers, @@ -76,7 +77,8 @@ public FloodgateHandshakeHandler( SkinUploadManager skinUploadManager, AttributeKey playerAttribute, FloodgateLogger logger, - LanguageManager languageManager) { + LanguageManager languageManager, + PendingPlayerManager pendingPlayerManager) { this.handshakeHandlers = handshakeHandlers; this.api = api; @@ -86,6 +88,7 @@ public FloodgateHandshakeHandler( this.playerAttribute = playerAttribute; this.logger = logger; this.languageManager = languageManager; + this.pendingPlayerManager = pendingPlayerManager; } /** @@ -241,6 +244,8 @@ private HandshakeResult handlePart2( InetSocketAddress socketAddress = new InetSocketAddress(handshakeData.getIp(), port); player.addProperty(PropertyKey.SOCKET_ADDRESS, socketAddress); + pendingPlayerManager.add(socketAddress, player); + return new HandshakeResult(ResultType.SUCCESS, handshakeData, bedrockData, player); } catch (Exception exception) { exception.printStackTrace(); diff --git a/core/src/main/java/org/geysermc/floodgate/player/PendingPlayerManager.java b/core/src/main/java/org/geysermc/floodgate/player/PendingPlayerManager.java new file mode 100644 index 00000000..2239a5dc --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/player/PendingPlayerManager.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019-2025 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.player; + +import jakarta.inject.Singleton; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.geysermc.floodgate.api.player.FloodgatePlayer; + +@Singleton +public class PendingPlayerManager { + + private final Map pendingByAddress = new ConcurrentHashMap<>(); + private final Map pendingByUsername = new ConcurrentHashMap<>(); + + /** + * Register a pending player after successful handshake decryption. + * Called from platform-specific handshake handlers. + * + * @param address the connection address + * @param player the FloodgatePlayer instance + */ + public void add(InetSocketAddress address, FloodgatePlayer player) { + pendingByAddress.put(address, player); + pendingByUsername.put(player.getUsername().toLowerCase(), player); + } + + /** + * Remove a pending player after login completion or disconnect. + * + * @param address the connection address + */ + public void remove(InetSocketAddress address) { + FloodgatePlayer player = pendingByAddress.remove(address); + if (player != null) { + pendingByUsername.remove(player.getUsername().toLowerCase()); + } + } + + /** + * Remove a pending player by username. + * + * @param rawUsername the raw username without prefix + */ + public void removeByUsername(String rawUsername) { + if (rawUsername == null) return; + FloodgatePlayer player = pendingByUsername.remove(rawUsername.toLowerCase()); + if (player != null) { + pendingByAddress.entrySet().removeIf(entry -> + entry.getValue().getUsername().equalsIgnoreCase(rawUsername)); + } + } + + /** + * Get a pending player by connection address. + * + * @param address the connection address + * @return the FloodgatePlayer or null if not found + */ + public FloodgatePlayer get(InetSocketAddress address) { + return pendingByAddress.get(address); + } + + /** + * Get a pending player by raw username (without prefix). + * + * @param rawUsername the raw username + * @return the FloodgatePlayer or null if not found + */ + public FloodgatePlayer getByUsername(String rawUsername) { + if (rawUsername == null) return null; + return pendingByUsername.get(rawUsername.toLowerCase()); + } +} diff --git a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java index 5ef02e92..bd15014b 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java +++ b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java @@ -26,15 +26,18 @@ package org.geysermc.floodgate.listener; import com.google.inject.Inject; +import java.net.InetSocketAddress; import java.util.UUID; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.geysermc.floodgate.api.SimpleFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.player.PendingPlayerManager; import org.geysermc.floodgate.skin.SkinApplier; import org.geysermc.floodgate.util.LanguageManager; import org.geysermc.floodgate.util.MojangUtils; @@ -43,6 +46,7 @@ public final class SpigotListener implements Listener { @Inject private SimpleFloodgateApi api; @Inject private LanguageManager languageManager; @Inject private FloodgateLogger logger; + @Inject private PendingPlayerManager pendingPlayerManager; @Inject private MojangUtils mojangUtils; @Inject private SkinApplier skinApplier; @@ -76,8 +80,15 @@ public void onPlayerJoin(PlayerJoinEvent event) { } } + @EventHandler + public void onPlayerLogin(PlayerLoginEvent event) { + InetSocketAddress address = event.getPlayer().getAddress(); + pendingPlayerManager.remove(address); + } + @EventHandler(priority = EventPriority.MONITOR) public void onPlayerQuit(PlayerQuitEvent event) { api.playerRemoved(event.getPlayer().getUniqueId()); + pendingPlayerManager.remove(event.getPlayer().getAddress()); } } 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 6c67a31f..2493550d 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java +++ b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java @@ -57,6 +57,7 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.ProxyFloodgateConfig; +import org.geysermc.floodgate.player.PendingPlayerManager; import org.geysermc.floodgate.skin.SkinDataImpl; import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.LanguageManager; @@ -117,6 +118,9 @@ public final class VelocityListener { @Inject private MojangUtils mojangUtils; + @Inject + private PendingPlayerManager pendingPlayerManager; + @Subscribe(order = PostOrder.EARLY) public void onPreLogin(PreLoginEvent event) { FloodgatePlayer player = null; @@ -197,10 +201,13 @@ public void onLogin(LoginEvent event) { languageManager.loadLocale(player.getLanguageCode()); } } + + pendingPlayerManager.remove(event.getPlayer().getRemoteAddress()); } @Subscribe(order = PostOrder.LAST) public void onDisconnect(DisconnectEvent event) { api.playerRemoved(event.getPlayer().getUniqueId()); + pendingPlayerManager.remove(event.getPlayer().getRemoteAddress()); } }