From 0c9527118abc3cbd0cd0eae10e5120e28311a383 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 1 Apr 2024 16:36:13 -0400 Subject: [PATCH] Add unstable API for custom Login Plugin Messages (#2074) * Add LoginPluginMessageBox to allow sending custom login plugin messages * throw in ConnectionManager because AsyncUtils has a try catch * Stack requests in AsyncPlayerPreLoginEvent so the user-facing API is scoped to the login stage * Fix addPluginRequest javadoc * feat: encapsulate velocityproxy logic, other minor tweaks * fix: revert velocityproxy changes --------- Co-authored-by: mworzala --- .../java/net/minestom/server/ServerFlag.java | 3 + .../player/AsyncPlayerPreLoginEvent.java | 21 ++++- .../minestom/server/extras/MojangAuth.java | 3 + .../server/extras/velocity/VelocityProxy.java | 5 ++ .../listener/preplay/LoginListener.java | 89 ++++++++++--------- .../server/network/ConnectionManager.java | 26 +++--- .../network/player/PlayerConnection.java | 17 ++++ .../player/PlayerSocketConnection.java | 44 --------- .../plugin/LoginPluginMessageProcessor.java | 64 +++++++++++++ .../network/plugin/LoginPluginRequest.java | 28 ++++++ .../network/plugin/LoginPluginResponse.java | 30 +++++++ 11 files changed, 231 insertions(+), 99 deletions(-) create mode 100644 src/main/java/net/minestom/server/network/plugin/LoginPluginMessageProcessor.java create mode 100644 src/main/java/net/minestom/server/network/plugin/LoginPluginRequest.java create mode 100644 src/main/java/net/minestom/server/network/plugin/LoginPluginResponse.java diff --git a/src/main/java/net/minestom/server/ServerFlag.java b/src/main/java/net/minestom/server/ServerFlag.java index f2e0256ba..cf6cb1749 100644 --- a/src/main/java/net/minestom/server/ServerFlag.java +++ b/src/main/java/net/minestom/server/ServerFlag.java @@ -25,6 +25,9 @@ public final class ServerFlag { public static final int PLAYER_PACKET_PER_TICK = Integer.getInteger("minestom.packet-per-tick", 20); public static final int PLAYER_PACKET_QUEUE_SIZE = Integer.getInteger("minestom.packet-queue-size", 1000); public static final int SEND_LIGHT_AFTER_BLOCK_PLACEMENT_DELAY = Integer.getInteger("minestom.send-light-after-block-placement-delay", 100); + public static final long KEEP_ALIVE_DELAY = Long.getLong("minestom.keep-alive-delay", 10_000); + public static final long KEEP_ALIVE_KICK = Long.getLong("minestom.keep-alive-kick", 30_000); + public static final long LOGIN_PLUGIN_MESSAGE_TIMEOUT = Long.getLong("minestom.login-plugin-message-timeout", 5_000); // Packet sending optimizations public static final boolean GROUPED_PACKET = PropertyUtils.getBoolean("minestom.grouped-packet", true); diff --git a/src/main/java/net/minestom/server/event/player/AsyncPlayerPreLoginEvent.java b/src/main/java/net/minestom/server/event/player/AsyncPlayerPreLoginEvent.java index acf28e4b6..7dee182dd 100644 --- a/src/main/java/net/minestom/server/event/player/AsyncPlayerPreLoginEvent.java +++ b/src/main/java/net/minestom/server/event/player/AsyncPlayerPreLoginEvent.java @@ -2,9 +2,12 @@ package net.minestom.server.event.player; import net.minestom.server.entity.Player; import net.minestom.server.event.trait.PlayerEvent; +import net.minestom.server.network.plugin.LoginPluginMessageProcessor; +import net.minestom.server.network.plugin.LoginPluginResponse; import org.jetbrains.annotations.NotNull; import java.util.UUID; +import java.util.concurrent.CompletableFuture; /** * Called before the player initialization, it can be used to kick the player before any connection @@ -13,11 +16,14 @@ import java.util.UUID; public class AsyncPlayerPreLoginEvent implements PlayerEvent { private final Player player; + private final LoginPluginMessageProcessor pluginMessageProcessor; + private String username; private UUID playerUuid; - public AsyncPlayerPreLoginEvent(@NotNull Player player) { + public AsyncPlayerPreLoginEvent(@NotNull Player player, @NotNull LoginPluginMessageProcessor pluginMessageProcessor) { this.player = player; + this.pluginMessageProcessor = pluginMessageProcessor; this.username = player.getUsername(); this.playerUuid = player.getUuid(); } @@ -60,6 +66,19 @@ public class AsyncPlayerPreLoginEvent implements PlayerEvent { this.playerUuid = playerUuid; } + /** + * Sends a login plugin message request. Can be useful to negotiate with modded clients or + * proxies before moving on to the Configuration state. + * + * @param channel the plugin message channel + * @param requestPayload the contents of the plugin message, can be null for empty + * + * @return a CompletableFuture for the response. The thread on which it completes is asynchronous. + */ + public @NotNull CompletableFuture sendPluginRequest(String channel, byte[] requestPayload) { + return pluginMessageProcessor.request(channel, requestPayload); + } + @Override public @NotNull Player getPlayer() { return player; diff --git a/src/main/java/net/minestom/server/extras/MojangAuth.java b/src/main/java/net/minestom/server/extras/MojangAuth.java index f87167b5b..92648da72 100644 --- a/src/main/java/net/minestom/server/extras/MojangAuth.java +++ b/src/main/java/net/minestom/server/extras/MojangAuth.java @@ -3,6 +3,7 @@ package net.minestom.server.extras; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; import net.minestom.server.extras.mojangAuth.MojangCrypt; +import net.minestom.server.extras.velocity.VelocityProxy; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.Nullable; @@ -21,6 +22,8 @@ public final class MojangAuth { public static void init() { Check.stateCondition(enabled, "Mojang auth is already enabled!"); Check.stateCondition(MinecraftServer.process().isAlive(), "The server has already been started!"); + Check.stateCondition(VelocityProxy.isEnabled(), "Velocity modern forwarding should not be enabled with MojangAuth"); + MojangAuth.enabled = true; // Generate necessary fields... MojangAuth.keyPair = MojangCrypt.generateKeyPair(); diff --git a/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java b/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java index c817ed425..10e47b03d 100644 --- a/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java +++ b/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java @@ -1,6 +1,8 @@ package net.minestom.server.extras.velocity; +import net.minestom.server.extras.MojangAuth; import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import javax.crypto.Mac; @@ -32,6 +34,9 @@ public final class VelocityProxy { * be sure to do not hardcode it in your code but to retrieve it from a file or anywhere else safe */ public static void enable(@NotNull String secret) { + Check.stateCondition(enabled, "Velocity modern forwarding is already enabled"); + Check.stateCondition(MojangAuth.isEnabled(), "Velocity modern forwarding should not be enabled with MojangAuth"); + VelocityProxy.enabled = true; VelocityProxy.key = new SecretKeySpec(secret.getBytes(), MAC_ALGORITHM); } diff --git a/src/main/java/net/minestom/server/listener/preplay/LoginListener.java b/src/main/java/net/minestom/server/listener/preplay/LoginListener.java index d05c6569e..15df756e7 100644 --- a/src/main/java/net/minestom/server/listener/preplay/LoginListener.java +++ b/src/main/java/net/minestom/server/listener/preplay/LoginListener.java @@ -19,10 +19,11 @@ import net.minestom.server.network.packet.client.login.ClientLoginPluginResponse import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; import net.minestom.server.network.packet.server.login.EncryptionRequestPacket; import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; -import net.minestom.server.network.packet.server.login.LoginPluginRequestPacket; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; +import net.minestom.server.network.plugin.LoginPluginMessageProcessor; +import net.minestom.server.network.plugin.LoginPluginResponse; import net.minestom.server.utils.async.AsyncUtils; import org.jetbrains.annotations.NotNull; @@ -44,6 +45,7 @@ public final class LoginListener { private static final Gson GSON = new Gson(); private static final Component ALREADY_CONNECTED = Component.text("You are already on this server", NamedTextColor.RED); + private static final Component ERROR_DURING_LOGIN = Component.text("Error during login!", NamedTextColor.RED); public static final Component INVALID_PROXY_RESPONSE = Component.text("Invalid proxy response!", NamedTextColor.RED); public static void loginStartListener(@NotNull ClientLoginStartPacket packet, @NotNull PlayerConnection connection) { @@ -54,11 +56,8 @@ public final class LoginListener { socketConnection.UNSAFE_setLoginUsername(packet.username()); // Velocity support if (VelocityProxy.isEnabled()) { - final int messageId = ThreadLocalRandom.current().nextInt(); - final String channel = VelocityProxy.PLAYER_INFO_CHANNEL; - // Important in order to retrieve the channel in the response packet - socketConnection.addPluginRequestEntry(messageId, channel); - connection.sendPacket(new LoginPluginRequestPacket(messageId, channel, null)); + connection.loginPluginMessageProcessor().request(VelocityProxy.PLAYER_INFO_CHANNEL, null) + .thenAccept(response -> handleVelocityProxyResponse(socketConnection, response)); return; } } @@ -163,48 +162,50 @@ public final class LoginListener { return MojangCrypt.decryptByteToSecretKey(MojangAuth.getKeyPair().getPrivate(), sharedSecret); } - public static void loginPluginResponseListener(@NotNull ClientLoginPluginResponsePacket packet, @NotNull PlayerConnection connection) { - // Proxy support - if (connection instanceof PlayerSocketConnection socketConnection) { - final String channel = socketConnection.getPluginRequestChannel(packet.messageId()); - if (channel != null) { - boolean success = false; + private static void handleVelocityProxyResponse(PlayerSocketConnection socketConnection, LoginPluginResponse response) { + byte[] data = response.getPayload(); - SocketAddress socketAddress = null; - GameProfile gameProfile = null; - - // Velocity - if (VelocityProxy.isEnabled() && channel.equals(VelocityProxy.PLAYER_INFO_CHANNEL)) { - byte[] data = packet.data(); - if (data != null && data.length > 0) { - NetworkBuffer buffer = new NetworkBuffer(ByteBuffer.wrap(data)); - success = VelocityProxy.checkIntegrity(buffer); - if (success) { - // Get the real connection address - final InetAddress address; - try { - address = InetAddress.getByName(buffer.read(STRING)); - } catch (UnknownHostException e) { - MinecraftServer.getExceptionManager().handleException(e); - return; - } - final int port = ((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort(); - socketAddress = new InetSocketAddress(address, port); - gameProfile = new GameProfile(buffer); - } - } - } - - if (success) { - socketConnection.setRemoteAddress(socketAddress); - socketConnection.UNSAFE_setProfile(gameProfile); - CONNECTION_MANAGER.createPlayer(connection, gameProfile.uuid(), gameProfile.name()); - } else { - LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE); - socketConnection.sendPacket(disconnectPacket); + SocketAddress socketAddress = null; + GameProfile gameProfile = null; + boolean success = false; + if (data != null && data.length > 0) { + NetworkBuffer buffer = new NetworkBuffer(ByteBuffer.wrap(data)); + success = VelocityProxy.checkIntegrity(buffer); + if (success) { + // Get the real connection address + final InetAddress address; + try { + address = InetAddress.getByName(buffer.read(STRING)); + } catch (UnknownHostException e) { + MinecraftServer.getExceptionManager().handleException(e); + return; } + final int port = ((java.net.InetSocketAddress) socketConnection.getRemoteAddress()).getPort(); + socketAddress = new InetSocketAddress(address, port); + gameProfile = new GameProfile(buffer); } } + + if (success) { + socketConnection.setRemoteAddress(socketAddress); + socketConnection.UNSAFE_setProfile(gameProfile); + CONNECTION_MANAGER.createPlayer(socketConnection, gameProfile.uuid(), gameProfile.name()); + } else { + LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE); + socketConnection.sendPacket(disconnectPacket); + } + } + + public static void loginPluginResponseListener(@NotNull ClientLoginPluginResponsePacket packet, @NotNull PlayerConnection connection) { + try { + LoginPluginMessageProcessor messageProcessor = connection.loginPluginMessageProcessor(); + messageProcessor.handleResponse(packet.messageId(), packet.data()); + } catch (Throwable t) { + MinecraftServer.LOGGER.error("Error handling Login Plugin Response", t); + LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(ERROR_DURING_LOGIN); + connection.sendPacket(disconnectPacket); + connection.disconnect(); + } } public static void loginAckListener(@NotNull ClientLoginAcknowledgedPacket ignored, @NotNull PlayerConnection connection) { diff --git a/src/main/java/net/minestom/server/network/ConnectionManager.java b/src/main/java/net/minestom/server/network/ConnectionManager.java index 8c82af5af..48508908f 100644 --- a/src/main/java/net/minestom/server/network/ConnectionManager.java +++ b/src/main/java/net/minestom/server/network/ConnectionManager.java @@ -3,12 +3,14 @@ package net.minestom.server.network; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; +import net.minestom.server.ServerFlag; import net.minestom.server.entity.Player; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.AsyncPlayerPreLoginEvent; import net.minestom.server.instance.Instance; +import net.minestom.server.listener.preplay.LoginListener; import net.minestom.server.message.Messenger; import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; import net.minestom.server.network.packet.server.common.KeepAlivePacket; @@ -20,6 +22,7 @@ import net.minestom.server.network.packet.server.login.LoginSuccessPacket; import net.minestom.server.network.packet.server.play.StartConfigurationPacket; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; +import net.minestom.server.network.plugin.LoginPluginMessageProcessor; import net.minestom.server.utils.StringUtils; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.debug.DebugUtils; @@ -30,26 +33,20 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jglrxavpok.hephaistos.nbt.NBT; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; import java.util.function.Function; /** * Manages the connected clients. */ public final class ConnectionManager { - private static final Logger logger = LoggerFactory.getLogger(ConnectionManager.class); - - private static final long KEEP_ALIVE_DELAY = 10_000; - private static final long KEEP_ALIVE_KICK = 30_000; private static final Component TIMEOUT_TEXT = Component.text("Timeout", NamedTextColor.RED); - // All players once their Player object has been instantiated. private final Map connectionPlayerMap = new ConcurrentHashMap<>(); // Players waiting to be spawned (post configuration state) @@ -225,7 +222,8 @@ public final class ConnectionManager { } // Call pre login event - AsyncPlayerPreLoginEvent asyncPlayerPreLoginEvent = new AsyncPlayerPreLoginEvent(player); + LoginPluginMessageProcessor pluginMessageProcessor = playerConnection.loginPluginMessageProcessor(); + AsyncPlayerPreLoginEvent asyncPlayerPreLoginEvent = new AsyncPlayerPreLoginEvent(player, pluginMessageProcessor); EventDispatcher.call(asyncPlayerPreLoginEvent); if (!player.isOnline()) return; // Player has been kicked @@ -242,6 +240,14 @@ public final class ConnectionManager { } } + // Wait for pending login plugin messages + try { + pluginMessageProcessor.awaitReplies(ServerFlag.LOGIN_PLUGIN_MESSAGE_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (Throwable t) { + player.kick(LoginListener.INVALID_PROXY_RESPONSE); + throw new RuntimeException("Error getting replies for login plugin messages", t); + } + // Send login success packet (and switch to configuration phase) LoginSuccessPacket loginSuccessPacket = new LoginSuccessPacket(player.getUuid(), player.getUsername(), 0); playerConnection.sendPacket(loginSuccessPacket); @@ -367,10 +373,10 @@ public final class ConnectionManager { final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(tickStart); for (Player player : playerGroup) { final long lastKeepAlive = tickStart - player.getLastKeepAlive(); - if (lastKeepAlive > KEEP_ALIVE_DELAY && player.didAnswerKeepAlive()) { + if (lastKeepAlive > ServerFlag.KEEP_ALIVE_DELAY && player.didAnswerKeepAlive()) { player.refreshKeepAlive(tickStart); player.sendPacket(keepAlivePacket); - } else if (lastKeepAlive >= KEEP_ALIVE_KICK) { + } else if (lastKeepAlive >= ServerFlag.KEEP_ALIVE_KICK) { player.kick(TIMEOUT_TEXT); } } diff --git a/src/main/java/net/minestom/server/network/player/PlayerConnection.java b/src/main/java/net/minestom/server/network/player/PlayerConnection.java index 5bd153736..4d2dc7e7b 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerConnection.java @@ -6,6 +6,7 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.packet.server.SendablePacket; +import net.minestom.server.network.plugin.LoginPluginMessageProcessor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import java.net.SocketAddress; import java.util.Collection; import java.util.List; +import java.util.Objects; /** * A PlayerConnection is an object needed for all created {@link Player}. @@ -24,6 +26,8 @@ public abstract class PlayerConnection { private PlayerPublicKey playerPublicKey; volatile boolean online; + private LoginPluginMessageProcessor loginPluginMessageProcessor = new LoginPluginMessageProcessor(this); + public PlayerConnection() { this.online = true; this.connectionState = ConnectionState.HANDSHAKE; @@ -141,6 +145,10 @@ public abstract class PlayerConnection { public void setConnectionState(@NotNull ConnectionState connectionState) { this.connectionState = connectionState; + if (connectionState == ConnectionState.CONFIGURATION) { + // Clear the plugin request map (it is not used beyond login) + this.loginPluginMessageProcessor = null; + } } /** @@ -160,6 +168,15 @@ public abstract class PlayerConnection { this.playerPublicKey = playerPublicKey; } + /** + * Gets the login plugin message processor, only available during the login state. + */ + @ApiStatus.Internal + public @NotNull LoginPluginMessageProcessor loginPluginMessageProcessor() { + return Objects.requireNonNull(this.loginPluginMessageProcessor, + "Login plugin message processor is only available during the login state."); + } + @Override public String toString() { return "PlayerConnection{" + diff --git a/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java b/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java index 3a30f1a3f..fefbf9c00 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java @@ -7,7 +7,6 @@ import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.ListenerHandle; import net.minestom.server.event.player.PlayerPacketOutEvent; import net.minestom.server.extras.mojangAuth.MojangCrypt; -import net.minestom.server.network.ConnectionState; import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.packet.client.ClientPacket; import net.minestom.server.network.packet.client.handshake.ClientHandshakePacket; @@ -34,7 +33,6 @@ import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.DataFormatException; @@ -66,10 +64,6 @@ public class PlayerSocketConnection extends PlayerConnection { private int serverPort; private int protocolVersion; - // Used for the login plugin request packet, to retrieve the channel from a message id, - // cleared once the player enters the play state - private final Map pluginRequestMap = new ConcurrentHashMap<>(); - private final List waitingBuffers = new ArrayList<>(); private final AtomicReference tickBuffer = new AtomicReference<>(POOL.get()); private BinaryBuffer cacheBuffer; @@ -287,44 +281,6 @@ public class PlayerSocketConnection extends PlayerConnection { this.protocolVersion = protocolVersion; } - /** - * Adds an entry to the plugin request map. - *

- * Only working if {@link #getConnectionState()} is {@link net.minestom.server.network.ConnectionState#LOGIN}. - * - * @param messageId the message id - * @param channel the packet channel - * @throws IllegalStateException if a messageId with the value {@code messageId} already exists for this connection - */ - public void addPluginRequestEntry(int messageId, @NotNull String channel) { - if (!getConnectionState().equals(ConnectionState.LOGIN)) { - return; - } - Check.stateCondition(pluginRequestMap.containsKey(messageId), "You cannot have two messageId with the same value"); - this.pluginRequestMap.put(messageId, channel); - } - - /** - * Gets a request channel from a message id, previously cached using {@link #addPluginRequestEntry(int, String)}. - *

- * Be aware that the internal map is cleared once the player enters the play state. - * - * @param messageId the message id - * @return the channel linked to the message id, null if not found - */ - public @Nullable String getPluginRequestChannel(int messageId) { - return pluginRequestMap.get(messageId); - } - - @Override - public void setConnectionState(@NotNull ConnectionState connectionState) { - super.setConnectionState(connectionState); - // Clear the plugin request map (since it is not used anymore) - if (connectionState.equals(ConnectionState.PLAY)) { - this.pluginRequestMap.clear(); - } - } - public byte[] getNonce() { return nonce; } diff --git a/src/main/java/net/minestom/server/network/plugin/LoginPluginMessageProcessor.java b/src/main/java/net/minestom/server/network/plugin/LoginPluginMessageProcessor.java new file mode 100644 index 000000000..5872450e5 --- /dev/null +++ b/src/main/java/net/minestom/server/network/plugin/LoginPluginMessageProcessor.java @@ -0,0 +1,64 @@ +package net.minestom.server.network.plugin; + +import net.minestom.server.network.packet.server.login.LoginPluginRequestPacket; +import net.minestom.server.network.player.PlayerConnection; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@ApiStatus.Internal +public class LoginPluginMessageProcessor { + private static final AtomicInteger REQUEST_ID = new AtomicInteger(0); + + private final Map requestByMsgId = new ConcurrentHashMap<>(); + private final PlayerConnection connection; + + public LoginPluginMessageProcessor(@NotNull PlayerConnection connection) { + this.connection = connection; + } + + public @NotNull CompletableFuture request(@NotNull String channel, byte @Nullable [] requestPayload) { + LoginPluginRequest request = new LoginPluginRequest(channel, requestPayload); + + int messageId = getNextMessageId(); + requestByMsgId.put(messageId, request); + connection.sendPacket(new LoginPluginRequestPacket(messageId, request.getChannel(), request.getRequestPayload())); + + return request.getResponseFuture(); + } + + public void handleResponse(int messageId, byte[] responseData) throws Exception { + LoginPluginRequest request = requestByMsgId.remove(messageId); + if (request == null) { + throw new Exception("Received unexpected Login Plugin Response id " + messageId + " of " + responseData.length + " bytes"); + } + + try { + LoginPluginResponse response = LoginPluginResponse.fromPayload(request.getChannel(), responseData); + request.getResponseFuture().complete(response); + } catch (Throwable t) { + throw new Exception("Error handling Login Plugin Response on channel '" + request.getChannel() + "'", t); + } + } + + public void awaitReplies(long timeout, @NotNull TimeUnit timeUnit) throws Exception { + if (requestByMsgId.isEmpty()) { + return; + } + + CompletableFuture[] futures = requestByMsgId.values().stream() + .map(LoginPluginRequest::getResponseFuture) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).get(timeout, timeUnit); + } + + private static int getNextMessageId() { + return REQUEST_ID.getAndIncrement(); + } +} diff --git a/src/main/java/net/minestom/server/network/plugin/LoginPluginRequest.java b/src/main/java/net/minestom/server/network/plugin/LoginPluginRequest.java new file mode 100644 index 000000000..bd699a3bb --- /dev/null +++ b/src/main/java/net/minestom/server/network/plugin/LoginPluginRequest.java @@ -0,0 +1,28 @@ +package net.minestom.server.network.plugin; + +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +public class LoginPluginRequest { + private final String channel; + private final byte[] requestPayload; + private final CompletableFuture responseFuture = new CompletableFuture<>(); + + public LoginPluginRequest(String channel, @Nullable byte[] requestPayload) { + this.channel = channel; + this.requestPayload = requestPayload; + } + + public String getChannel() { + return channel; + } + + public @Nullable byte[] getRequestPayload() { + return requestPayload; + } + + public CompletableFuture getResponseFuture() { + return responseFuture; + } +} diff --git a/src/main/java/net/minestom/server/network/plugin/LoginPluginResponse.java b/src/main/java/net/minestom/server/network/plugin/LoginPluginResponse.java new file mode 100644 index 000000000..1dc92a301 --- /dev/null +++ b/src/main/java/net/minestom/server/network/plugin/LoginPluginResponse.java @@ -0,0 +1,30 @@ +package net.minestom.server.network.plugin; + +public class LoginPluginResponse { + private final String channel; + private final boolean understood; + private final byte[] payload; + + private LoginPluginResponse(String channel, boolean understood, byte[] payload) { + this.channel = channel; + this.understood = understood; + this.payload = payload; + } + + public String getChannel() { + return channel; + } + + public boolean isUnderstood() { + return understood; + } + + public byte[] getPayload() { + return payload; + } + + public static LoginPluginResponse fromPayload(String channel, byte[] payload) { + boolean understood = payload != null; + return new LoginPluginResponse(channel, understood, payload); + } +}