mirror of https://github.com/Minestom/Minestom.git
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 <mattheworzala@gmail.com>
This commit is contained in:
parent
63f02929ed
commit
0c9527118a
|
@ -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);
|
||||
|
|
|
@ -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<LoginPluginResponse> sendPluginRequest(String channel, byte[] requestPayload) {
|
||||
return pluginMessageProcessor.request(channel, requestPayload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Player getPlayer() {
|
||||
return player;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<PlayerConnection, Player> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{" +
|
||||
|
|
|
@ -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<Integer, String> pluginRequestMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final List<BinaryBuffer> waitingBuffers = new ArrayList<>();
|
||||
private final AtomicReference<BinaryBuffer> 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.
|
||||
* <p>
|
||||
* 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)}.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -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<Integer, LoginPluginRequest> requestByMsgId = new ConcurrentHashMap<>();
|
||||
private final PlayerConnection connection;
|
||||
|
||||
public LoginPluginMessageProcessor(@NotNull PlayerConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public @NotNull CompletableFuture<LoginPluginResponse> 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();
|
||||
}
|
||||
}
|
|
@ -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<LoginPluginResponse> 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<LoginPluginResponse> getResponseFuture() {
|
||||
return responseFuture;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue