diff --git a/.gitignore b/.gitignore index fbdf3cea..5001468c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.iml .idea_modules/ atlassian-ide-plugin.xml +.fleet/ ### Eclipse ### .metadata diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index d7a616f7..863b77e7 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -23,11 +23,11 @@ public enum ProtocolVersion { UNKNOWN(-1, "Unknown"), LEGACY(-2, "Legacy"), MINECRAFT_1_7_2(4, - "1.7.2", "1.7.3", "1.7.4", "1.7.5"), + "1.7.2", "1.7.3", "1.7.4", "1.7.5"), MINECRAFT_1_7_6(5, - "1.7.6", "1.7.7", "1.7.8", "1.7.9", "1.7.10"), + "1.7.6", "1.7.7", "1.7.8", "1.7.9", "1.7.10"), MINECRAFT_1_8(47, - "1.8", "1.8.1", "1.8.2", "1.8.3", "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9"), + "1.8", "1.8.1", "1.8.2", "1.8.3", "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9"), MINECRAFT_1_9(107, "1.9"), MINECRAFT_1_9_1(108, "1.9.1"), MINECRAFT_1_9_2(109, "1.9.2"), @@ -59,7 +59,8 @@ public enum ProtocolVersion { MINECRAFT_1_18(757, "1.18", "1.18.1"), MINECRAFT_1_18_2(758, "1.18.2"), MINECRAFT_1_19(759, "1.19"), - MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"); + MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), + MINECRAFT_1_19_3(761, 114, "1.19.3"); private static final int SNAPSHOT_BIT = 30; @@ -80,8 +81,8 @@ public enum ProtocolVersion { * The user-friendly representation of the lowest and highest supported versions. */ public static final String SUPPORTED_VERSION_STRING = String - .format("%s-%s", MINIMUM_VERSION.getVersionIntroducedIn(), - MAXIMUM_VERSION.getMostRecentSupportedVersion()); + .format("%s-%s", MINIMUM_VERSION.getVersionIntroducedIn(), + MAXIMUM_VERSION.getMostRecentSupportedVersion()); /** * A map linking the protocol version number to its {@link ProtocolVersion} representation. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java b/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java index 9050a61a..665853ae 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java @@ -59,7 +59,7 @@ public interface IdentifiedKey extends KeySigned { final Set backwardsCompatibleTo; final Set applicableTo; - + Revision(Set backwardsCompatibleTo, Set applicableTo) { this.backwardsCompatibleTo = backwardsCompatibleTo; this.applicableTo = applicableTo; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/ChatSession.java b/api/src/main/java/com/velocitypowered/api/proxy/player/ChatSession.java new file mode 100644 index 00000000..85f579ae --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/ChatSession.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.proxy.player; + +import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; +import java.util.UUID; + +/** + * Represents a chat session held by a player. + */ +public interface ChatSession extends KeyIdentifiable { + /** + * Returns the {@link UUID} of the session. + * + * @return the session UUID + */ + UUID getSessionId(); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java index 1f386b3e..02e16c77 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -68,32 +68,53 @@ public interface TabList { */ Collection getEntries(); + /** + * Clears all entries from the tab list. + */ + void clearAll(); + /** * Builds a tab list entry. * - * @param profile profile + * @param profile profile * @param displayName display name - * @param latency latency - * @param gameMode game mode + * @param latency latency + * @param gameMode game mode * @return entry * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. */ @Deprecated TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, - int gameMode); + int gameMode); /** * Builds a tab list entry. * - * @param profile profile + * @param profile profile * @param displayName display name - * @param latency latency - * @param gameMode game mode - * @param key the player key + * @param latency latency + * @param gameMode game mode + * @param key the player key * @return entry - * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. + * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. */ @Deprecated TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode, @Nullable IdentifiedKey key); + + + /** + * Represents an entry in a {@link Player}'s tab list. + * + * @param profile the profile + * @param displayName the display name + * @param latency the latency + * @param gameMode the game mode + * @param chatSession the chat session + * @return the entry + * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. + */ + @Deprecated + TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, + int gameMode, @Nullable ChatSession chatSession); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java index abf503d6..5714a205 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java @@ -18,6 +18,21 @@ import org.checkerframework.checker.nullness.qual.Nullable; * Represents a single entry in a {@link TabList}. */ public interface TabListEntry extends KeyIdentifiable { + /** + * Returns the {@link ChatSession} associated with this entry. + * + * @return the chat session + */ + @Nullable ChatSession getChatSession(); + + @Override + default IdentifiedKey getIdentifiedKey() { + ChatSession session = getChatSession(); + if (session == null) { + return null; + } + return getChatSession().getIdentifiedKey(); + } /** * Returns the parent {@link TabList} of this {@code this} {@link TabListEntry}. @@ -41,7 +56,7 @@ public interface TabListEntry extends KeyIdentifiable { * {@link GameProfile#getName()} is shown. * * @return {@link Optional} text {@link net.kyori.adventure.text.Component} of name displayed in - * the tab list + * the tab list */ Optional getDisplayNameComponent(); @@ -105,6 +120,25 @@ public interface TabListEntry extends KeyIdentifiable { */ TabListEntry setGameMode(int gameMode); + /** + * Whether or not the entry is listed, when listed they will be visible to other players in the tab list. + * + * @return Whether this entry is listed; only changeable in 1.19.3 and above + */ + default boolean isListed() { + return true; + } + + /** + * Sets whether this entry is listed. + * + * @param listed whether this entry is listed + * @return {@code this}, for chaining + */ + default TabListEntry setListed(boolean listed) { + return this; + } + /** * Returns a {@link Builder} to create a {@link TabListEntry}. * @@ -127,7 +161,7 @@ public interface TabListEntry extends KeyIdentifiable { private int latency = 0; private int gameMode = 0; - private @Nullable IdentifiedKey playerKey; + private @Nullable ChatSession chatSession; private Builder() { } @@ -162,12 +196,12 @@ public interface TabListEntry extends KeyIdentifiable { *

For any player currently connected to this proxy this will be filled automatically.

*

Will ignore mismatching key revisions data.

* - * @param playerKey key to set + * @param chatSession session to set * @return {@code this}, for chaining - * @see TabListEntry#getIdentifiedKey() + * @see TabListEntry#getChatSession() */ - public Builder playerKey(IdentifiedKey playerKey) { - this.playerKey = playerKey; + public Builder chatSession(ChatSession chatSession) { + this.chatSession = chatSession; return this; } @@ -219,7 +253,7 @@ public interface TabListEntry extends KeyIdentifiable { if (profile == null) { throw new IllegalStateException("The GameProfile must be set when building a TabListEntry"); } - return tabList.buildEntry(profile, displayName, latency, gameMode, playerKey); + return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index b3a15e05..e7205edb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -30,10 +30,11 @@ import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyHandshake; import com.velocitypowered.proxy.protocol.packet.LegacyPing; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem; import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; @@ -46,14 +47,14 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; -import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletion; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatPreview; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; -import com.velocitypowered.proxy.protocol.packet.chat.ServerChatPreview; -import com.velocitypowered.proxy.protocol.packet.chat.ServerPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -228,7 +229,7 @@ public interface MinecraftSessionHandler { return false; } - default boolean handle(PlayerListItem packet) { + default boolean handle(LegacyPlayerListItem packet) { return false; } @@ -240,7 +241,11 @@ public interface MinecraftSessionHandler { return false; } - default boolean handle(PlayerChat packet) { + default boolean handle(KeyedPlayerChat packet) { + return false; + } + + default boolean handle(SessionPlayerChat packet) { return false; } @@ -248,27 +253,27 @@ public interface MinecraftSessionHandler { return false; } - default boolean handle(ServerPlayerChat packet) { + default boolean handle(KeyedPlayerCommand packet) { return false; } - default boolean handle(PlayerChatPreview packet) { - return false; - } - - default boolean handle(ServerChatPreview packet) { - return false; - } - - default boolean handle(PlayerCommand packet) { + default boolean handle(SessionPlayerCommand packet) { return false; } default boolean handle(PlayerChatCompletion packet) { return false; } - + default boolean handle(ServerData serverData) { return false; } + + default boolean handle(RemovePlayerInfo packet) { + return false; + } + + default boolean handle(UpsertPlayerInfo packet) { + return false; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java index 76851b56..e95996b0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java @@ -27,7 +27,8 @@ public class VelocityConstants { public static final int MODERN_FORWARDING_DEFAULT = 1; public static final int MODERN_FORWARDING_WITH_KEY = 2; public static final int MODERN_FORWARDING_WITH_KEY_V2 = 3; - public static final int MODERN_FORWARDING_MAX_VERSION = MODERN_FORWARDING_WITH_KEY_V2; + public static final int MODERN_LAZY_SESSION = 4; + public static final int MODERN_FORWARDING_MAX_VERSION = MODERN_LAZY_SESSION; public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index d2d65477..0be9ab12 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -43,12 +43,14 @@ import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.KeepAlive; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -60,7 +62,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class BackendPlaySessionHandler implements MinecraftSessionHandler { - private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class); private static final boolean BACKPRESSURE_LOG = Boolean @@ -140,10 +141,10 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ResourcePackRequest packet) { ResourcePackInfo.Builder builder = new VelocityResourcePackInfo.BuilderImpl( - Preconditions.checkNotNull(packet.getUrl())) - .setPrompt(packet.getPrompt()) - .setShouldForce(packet.isRequired()) - .setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + Preconditions.checkNotNull(packet.getUrl())) + .setPrompt(packet.getPrompt()) + .setShouldForce(packet.isRequired()) + .setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); String hash = packet.getHash(); if (hash != null && !hash.isEmpty()) { @@ -153,7 +154,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } ServerResourcePackSendEvent event = new ServerResourcePackSendEvent( - builder.build(), this.serverConn); + builder.build(), this.serverConn); server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackSendEvent -> { if (playerConnection.isClosed()) { @@ -163,7 +164,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); if (toSend != serverResourcePackSendEvent.getReceivedResourcePack()) { ((VelocityResourcePackInfo) toSend) - .setOriginalOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + .setOriginalOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); } serverConn.getPlayer().queueResourcePack(toSend); @@ -176,8 +177,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { }, playerConnection.eventLoop()).exceptionally((ex) -> { if (serverConn.getConnection() != null) { serverConn.getConnection().write(new ResourcePackResponse( - packet.getHash(), - PlayerResourcePackStatusEvent.Status.DECLINED + packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED )); } logger.error("Exception while handling resource pack send for {}", playerConnection, ex); @@ -245,8 +246,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } @Override - public boolean handle(PlayerListItem packet) { - serverConn.getPlayer().getTabList().processBackendPacket(packet); + public boolean handle(LegacyPlayerListItem packet) { + serverConn.getPlayer().getTabList().processLegacy(packet); + return false; + } + + @Override + public boolean handle(UpsertPlayerInfo packet) { + serverConn.getPlayer().getTabList().processUpdate(packet); + return false; + } + + @Override + public boolean handle(RemovePlayerInfo packet) { + serverConn.getPlayer().getTabList().processRemove(packet); return false; } @@ -260,7 +273,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } server.getEventManager().fire( - new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode)) + new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode)) .thenAcceptAsync(event -> playerConnection.write(commands), playerConnection.eventLoop()) .exceptionally((ex) -> { logger.error("Exception while handling available commands for {}", playerConnection, ex); @@ -280,7 +293,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { this.playerConnection.write( new ServerData(pingEvent.getPing().getDescriptionComponent(), pingEvent.getPing().getFavicon().orElse(null), - packet.isPreviewsChat(), packet.isSecureChatEnforced()) + packet.isSecureChatEnforced()) ), playerConnection.eventLoop()); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 310f6269..c23640cb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -17,12 +17,10 @@ package com.velocitypowered.proxy.connection.backend; -import com.google.common.base.Preconditions; import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; -import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.VelocityConfiguration; @@ -51,9 +49,11 @@ import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import net.kyori.adventure.text.Component; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class LoginSessionHandler implements MinecraftSessionHandler { + private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); private static final Component MODERN_IP_FORWARDING_FAILURE = Component .translatable("velocity.error.modern-forwarding-failed"); @@ -177,6 +177,10 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // Ensure we are in range requested = Math.min(requested, VelocityConstants.MODERN_FORWARDING_MAX_VERSION); if (requested > VelocityConstants.MODERN_FORWARDING_DEFAULT) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + return requested >= VelocityConstants.MODERN_LAZY_SESSION ? VelocityConstants.MODERN_LAZY_SESSION + : VelocityConstants.MODERN_FORWARDING_DEFAULT; + } if (player.getIdentifiedKey() != null) { // No enhanced switch on java 11 switch (player.getIdentifiedKey().getKeyRevision()) { @@ -210,7 +214,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // This serves as additional redundancy. The key normally is stored in the // login start to the server, but some setups require this. - if (actualVersion >= VelocityConstants.MODERN_FORWARDING_WITH_KEY) { + if (actualVersion >= VelocityConstants.MODERN_FORWARDING_WITH_KEY + && actualVersion < VelocityConstants.MODERN_LAZY_SESSION) { IdentifiedKey key = player.getIdentifiedKey(); assert key != null; ProtocolUtils.writePlayerKey(forwarded, key); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index cde9abe2..fde61127 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -190,7 +190,12 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, mc.setProtocolVersion(protocolVersion); mc.setState(StateRegistry.LOGIN); - mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getIdentifiedKey())); + if (proxyPlayer.getIdentifiedKey() == null + && proxyPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getUniqueId())); + } else { + mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getIdentifiedKey())); + } mc.flush(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 05d3925f..8c21ba3a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -25,14 +25,11 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc import com.google.common.collect.ImmutableList; import com.mojang.brigadier.suggestion.Suggestion; import com.velocitypowered.api.command.VelocityBrigadierMessage; -import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; -import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; @@ -43,8 +40,6 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases; import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.crypto.SignedChatCommand; -import com.velocitypowered.proxy.crypto.SignedChatMessage; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.BossBar; @@ -57,11 +52,20 @@ import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer; -import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder; -import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; -import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; +import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler; +import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper; +import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedChatHandler; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedCommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatHandler; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyCommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionChatHandler; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionCommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; @@ -73,10 +77,8 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.Queue; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -98,7 +100,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final Queue loginPluginMessages = new ConcurrentLinkedQueue<>(); private final VelocityServer server; private @Nullable TabCompleteRequest outstandingTabComplete; - private @Nullable Instant lastChatMessage; // Added in 1.19 + private final ChatHandler chatHandler; + private final CommandHandler commandHandler; + private final ChatTimeKeeper timeKeeper = new ChatTimeKeeper(); /** * Constructs a client play session handler. @@ -109,127 +113,40 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) { this.player = player; this.server = server; + + if (this.player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + this.chatHandler = new SessionChatHandler(this.player, this.server); + this.commandHandler = new SessionCommandHandler(this.player, this.server); + } else if (this.player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + this.chatHandler = new KeyedChatHandler(this.server, this.player); + this.commandHandler = new KeyedCommandHandler(this.player, this.server); + } else { + this.chatHandler = new LegacyChatHandler(this.server, this.player); + this.commandHandler = new LegacyCommandHandler(this.player, this.server); + } } - // I will not allow hacks to bypass this; - private boolean tickLastMessage(SignedChatMessage nextMessage) { - if (lastChatMessage != null && lastChatMessage.isAfter(nextMessage.getExpiryTemporal())) { + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean updateTimeKeeper(Instant instant) { + if (!this.timeKeeper.update(instant)) { player.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat")); return false; } - - lastChatMessage = nextMessage.getExpiryTemporal(); return true; } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean validateChat(String message) { if (CharacterUtil.containsIllegalCharacters(message)) { - player.disconnect(Component.translatable("velocity.error.illegal-chat-characters", - NamedTextColor.RED)); + player.disconnect(Component.translatable("velocity.error.illegal-chat-characters", NamedTextColor.RED)); return false; } return true; } - private MinecraftConnection retrieveServerConnection() { - VelocityServerConnection serverConnection = player.getConnectedServer(); - if (serverConnection == null) { - return null; - } - return serverConnection.getConnection(); - } - - private void processCommandMessage(String message, @Nullable SignedChatCommand signedCommand, - MinecraftPacket original, Instant passedTimestamp) { - this.player.getChatQueue().queuePacket(server.getCommandManager().callCommandEvent(player, message) - .thenComposeAsync(event -> processCommandExecuteResult(message, - event.getResult(), signedCommand, passedTimestamp)) - .whenComplete((ignored, throwable) -> { - if (server.getConfiguration().isLogCommandExecutions()) { - logger.info("{} -> executed command /{}", player, message); - } - }) - .exceptionally(e -> { - logger.info("Exception occurred while running command for {}", - player.getUsername(), e); - player.sendMessage(Component.translatable("velocity.command.generic-error", - NamedTextColor.RED)); - return null; - }), passedTimestamp); - } - - private void processPlayerChat(String message, @Nullable SignedChatMessage signedMessage, - MinecraftPacket original) { - MinecraftConnection smc = retrieveServerConnection(); - if (smc == null) { - return; - } - - if (signedMessage == null) { - PlayerChatEvent event = new PlayerChatEvent(player, message); - callChat(original, event, null).thenAccept(smc::write); - } else { - Instant messageTimestamp = signedMessage.getExpiryTemporal(); - PlayerChatEvent event = new PlayerChatEvent(player, message); - this.player.getChatQueue().queuePacket(callChat(original, event, signedMessage), messageTimestamp); - } - } - - private CompletableFuture callChat(MinecraftPacket original, PlayerChatEvent event, - @Nullable SignedChatMessage signedMessage) { - return server.getEventManager().fire(event) - .thenApply(pme -> { - PlayerChatEvent.ChatResult chatResult = pme.getResult(); - IdentifiedKey playerKey = player.getIdentifiedKey(); - if (chatResult.isAllowed()) { - Optional eventMsg = pme.getResult().getMessage(); - if (eventMsg.isPresent()) { - String messageNew = eventMsg.get(); - if (playerKey != null) { - if (signedMessage != null && !messageNew.equals(signedMessage.getMessage())) { - if (playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { - // Bad, very bad. - logger.fatal("A plugin tried to change a signed chat message. " - + "This is no longer possible in 1.19.1 and newer. " - + "Disconnecting player " + player.getUsername()); - player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - } else { - logger.warn("A plugin changed a signed chat message. The server may not accept it."); - return ChatBuilder.builder(player.getProtocolVersion()) - .message(messageNew).toServer(); - } - } else { - return original; - } - } else { - return ChatBuilder.builder(player.getProtocolVersion()) - .message(messageNew).toServer(); - } - } else { - return original; - } - } else { - if (playerKey != null && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { - logger.fatal("A plugin tried to cancel a signed chat message." - + " This is no longer possible in 1.19.1 and newer. " - + "Disconnecting player " + player.getUsername()); - player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - } - } - return null; - }) - .exceptionally((ex) -> { - logger.error("Exception while handling player chat for {}", player, ex); - return null; - }); - } - @Override public void activated() { - Collection channels = server.getChannelRegistrar().getChannelsForProtocol(player - .getProtocolVersion()); + Collection channels = server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels); player.getConnection().write(register); @@ -267,46 +184,63 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } @Override - public boolean handle(PlayerCommand packet) { + public boolean handle(SessionPlayerCommand packet) { player.ensureAndGetCurrentServer(); + + if (!updateTimeKeeper(packet.getTimeStamp())) { + return true; + } + if (!validateChat(packet.getCommand())) { return true; } - if (!packet.isUnsigned()) { - SignedChatCommand signedCommand = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false); - if (signedCommand != null) { - processCommandMessage(packet.getCommand(), signedCommand, packet, packet.getTimestamp()); - return true; - } - } - processCommandMessage(packet.getCommand(), null, packet, packet.getTimestamp()); - return true; + return this.commandHandler.handlePlayerCommand(packet); } @Override - public boolean handle(PlayerChat packet) { + public boolean handle(SessionPlayerChat packet) { player.ensureAndGetCurrentServer(); + + if (!updateTimeKeeper(packet.getTimestamp())) { + return true; + } + if (!validateChat(packet.getMessage())) { return true; } - if (!packet.isUnsigned()) { - // Bad if spoofed - SignedChatMessage signedChat = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false); - if (signedChat != null) { - // Server doesn't care for expiry as long as order is correct - if (!tickLastMessage(signedChat)) { - return true; - } + return this.chatHandler.handlePlayerChat(packet); + } - processPlayerChat(packet.getMessage(), signedChat, packet); - return true; - } + @Override + public boolean handle(KeyedPlayerCommand packet) { + player.ensureAndGetCurrentServer(); + + if (!updateTimeKeeper(packet.getTimestamp())) { + return true; } - processPlayerChat(packet.getMessage(), null, packet); - return true; + if (!validateChat(packet.getCommand())) { + return true; + } + + return this.commandHandler.handlePlayerCommand(packet); + } + + @Override + public boolean handle(KeyedPlayerChat packet) { + player.ensureAndGetCurrentServer(); + + if (!updateTimeKeeper(packet.getExpiry())) { + return true; + } + + if (!validateChat(packet.getMessage())) { + return true; + } + + return this.chatHandler.handlePlayerChat(packet); } @Override @@ -318,9 +252,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } if (msg.startsWith("/")) { - processCommandMessage(msg.substring(1), null, packet, Instant.now()); + this.commandHandler.handlePlayerCommand(packet); } else { - processPlayerChat(msg, null, packet); + this.chatHandler.handlePlayerChat(packet); } return true; } @@ -342,8 +276,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null; if (serverConn != null && backendConn != null) { if (backendConn.getState() != StateRegistry.PLAY) { - logger.warn("A plugin message was received while the backend server was not " - + "ready. Channel: {}. Packet discarded.", packet.getChannel()); + logger.warn( + "A plugin message was received while the backend server was not " + "ready. Channel: {}. Packet discarded.", + packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); player.getKnownChannels().addAll(channels); @@ -355,8 +290,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { channelIdentifiers.add(new LegacyChannelIdentifier(channel)); } } - server.getEventManager().fireAndForget(new PlayerChannelRegisterEvent(player, - ImmutableList.copyOf(channelIdentifiers))); + server.getEventManager() + .fireAndForget(new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); @@ -365,8 +300,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String brand = PluginMessageUtil.readBrandMessage(packet.content()); server.getEventManager().fireAndForget(new PlayerClientBrandEvent(player, brand)); player.setClientBrand(brand); - backendConn.write(PluginMessageUtil - .rewriteMinecraftBrand(packet, server.getVersion(), player.getProtocolVersion())); + backendConn.write( + PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), player.getProtocolVersion())); } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { return true; } else { @@ -401,8 +336,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy); server.getEventManager().fire(event).thenAcceptAsync(pme -> { if (pme.getResult().isAllowed()) { - PluginMessage message = new PluginMessage(packet.getChannel(), - Unpooled.wrappedBuffer(copy)); + PluginMessage message = new PluginMessage(packet.getChannel(), Unpooled.wrappedBuffer(copy)); if (!player.getPhase().consideredComplete() || !serverConn.getPhase().consideredComplete()) { // We're still processing the connection (see above), enqueue the packet for now. loginPluginMessages.add(message.retain()); @@ -410,12 +344,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { backendConn.write(message); } } - }, backendConn.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling plugin message packet for {}", - player, ex); - return null; - }); + }, backendConn.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message packet for {}", player, ex); + return null; + }); } } } @@ -467,8 +399,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void exception(Throwable throwable) { - player.disconnect(Component.translatable("velocity.error.player-connection-error", - NamedTextColor.RED)); + player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); } @Override @@ -548,8 +479,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Clear any title from the previous server. if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { - player.getConnection().delayedWrite(GenericTitlePacket.constructTitlePacket( - GenericTitlePacket.ActionType.RESET, player.getProtocolVersion())); + player.getConnection().delayedWrite( + GenericTitlePacket.constructTitlePacket(GenericTitlePacket.ActionType.RESET, player.getProtocolVersion())); } // Flush everything @@ -619,37 +550,33 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return false; } - server.getCommandManager().offerBrigadierSuggestions(player, command) - .thenAcceptAsync(suggestions -> { - if (suggestions.isEmpty()) { - return; - } + server.getCommandManager().offerBrigadierSuggestions(player, command).thenAcceptAsync(suggestions -> { + if (suggestions.isEmpty()) { + return; + } - List offers = new ArrayList<>(); - for (Suggestion suggestion : suggestions.getList()) { - String offer = suggestion.getText(); - Component tooltip = null; - if (suggestion.getTooltip() != null - && suggestion.getTooltip() instanceof VelocityBrigadierMessage) { - tooltip = ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent(); - } - offers.add(new Offer(offer, tooltip)); - } - int startPos = packet.getCommand().lastIndexOf(' ') + 1; - if (startPos > 0) { - TabCompleteResponse resp = new TabCompleteResponse(); - resp.setTransactionId(packet.getTransactionId()); - resp.setStart(startPos); - resp.setLength(packet.getCommand().length() - startPos); - resp.getOffers().addAll(offers); - player.getConnection().write(resp); - } - }, player.getConnection().eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling command tab completion for player {} executing {}", - player, command, ex); - return null; - }); + List offers = new ArrayList<>(); + for (Suggestion suggestion : suggestions.getList()) { + String offer = suggestion.getText(); + Component tooltip = null; + if (suggestion.getTooltip() != null && suggestion.getTooltip() instanceof VelocityBrigadierMessage) { + tooltip = ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent(); + } + offers.add(new Offer(offer, tooltip)); + } + int startPos = packet.getCommand().lastIndexOf(' ') + 1; + if (startPos > 0) { + TabCompleteResponse resp = new TabCompleteResponse(); + resp.setTransactionId(packet.getTransactionId()); + resp.setStart(startPos); + resp.setLength(packet.getCommand().length() - startPos); + resp.getOffers().addAll(offers); + player.getConnection().write(resp); + } + }, player.getConnection().eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling command tab completion for player {} executing {}", player, command, ex); + return null; + }); return true; // Sorry, handler; we're just gonna have to lie to you here. } @@ -683,37 +610,32 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) { String command = request.getCommand().substring(1); - server.getCommandManager().offerBrigadierSuggestions(player, command) - .thenAcceptAsync(offers -> { - boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0; - try { - for (Suggestion suggestion : offers.getList()) { - String offer = suggestion.getText(); - offer = legacy && !offer.startsWith("/") ? "/" + offer : offer; - if (legacy && offer.startsWith(command)) { - offer = offer.substring(command.length()); - } - Component tooltip = null; - if (suggestion.getTooltip() != null - && suggestion.getTooltip() instanceof VelocityBrigadierMessage) { - tooltip = ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent(); - } - response.getOffers().add(new Offer(offer, tooltip)); - } - response.getOffers().sort(null); - player.getConnection().write(response); - } catch (Exception e) { - logger.error("Unable to provide tab list completions for {} for command '{}'", - player.getUsername(), - command, e); + server.getCommandManager().offerBrigadierSuggestions(player, command).thenAcceptAsync(offers -> { + boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0; + try { + for (Suggestion suggestion : offers.getList()) { + String offer = suggestion.getText(); + offer = legacy && !offer.startsWith("/") ? "/" + offer : offer; + if (legacy && offer.startsWith(command)) { + offer = offer.substring(command.length()); } - }, player.getConnection().eventLoop()) - .exceptionally((ex) -> { - logger.error( - "Exception while finishing command tab completion, with request {} and response {}", - request, response, ex); - return null; - }); + Component tooltip = null; + if (suggestion.getTooltip() != null && suggestion.getTooltip() instanceof VelocityBrigadierMessage) { + tooltip = ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent(); + } + response.getOffers().add(new Offer(offer, tooltip)); + } + response.getOffers().sort(null); + player.getConnection().write(response); + } catch (Exception e) { + logger.error("Unable to provide tab list completions for {} for command '{}'", player.getUsername(), command, + e); + } + }, player.getConnection().eventLoop()).exceptionally((ex) -> { + logger.error("Exception while finishing command tab completion, with request {} and response {}", request, + response, ex); + return null; + }); } private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) { @@ -721,96 +643,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { for (Offer offer : response.getOffers()) { offers.add(offer.getText()); } - server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers)) - .thenAcceptAsync(e -> { - response.getOffers().clear(); - for (String s : e.getSuggestions()) { - response.getOffers().add(new Offer(s)); - } - player.getConnection().write(response); - }, player.getConnection().eventLoop()) - .exceptionally((ex) -> { - logger.error( - "Exception while finishing regular tab completion, with request {} and response{}", - request, response, ex); - return null; - }); - } - - - private CompletableFuture processCommandExecuteResult(String originalCommand, - CommandResult result, - @Nullable SignedChatCommand signedCommand, - Instant passedTimestamp) { - IdentifiedKey playerKey = player.getIdentifiedKey(); - if (result == CommandResult.denied()) { - if (playerKey != null) { - if (signedCommand != null && playerKey.getKeyRevision() - .compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { - logger.fatal("A plugin tried to deny a command with signable component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername()); - player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - } + server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers)).thenAcceptAsync(e -> { + response.getOffers().clear(); + for (String s : e.getSuggestions()) { + response.getOffers().add(new Offer(s)); } - return CompletableFuture.completedFuture(null); - } - - String commandToRun = result.getCommand().orElse(originalCommand); - if (result.isForwardToServer()) { - ChatBuilder write = ChatBuilder - .builder(player.getProtocolVersion()) - .timestamp(passedTimestamp) - .asPlayer(player); - - if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) { - write.message(signedCommand); - } else { - if (signedCommand != null && playerKey != null && playerKey.getKeyRevision() - .compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { - logger.fatal("A plugin tried to change a command with signed component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername()); - player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - return CompletableFuture.completedFuture(null); - } - write.message("/" + commandToRun); - } - return CompletableFuture.completedFuture(write.toServer()); - } else { - return server.getCommandManager().executeImmediatelyAsync(player, commandToRun) - .thenApply(hasRun -> { - if (!hasRun) { - ChatBuilder write = ChatBuilder - .builder(player.getProtocolVersion()) - .timestamp(passedTimestamp) - .asPlayer(player); - - if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) { - write.message(signedCommand); - } else { - if (signedCommand != null && playerKey != null && playerKey.getKeyRevision() - .compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { - logger.fatal("A plugin tried to change a command with signed component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername()); - player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - return null; - } - write.message("/" + commandToRun); - } - return write.toServer(); - } - return null; - }); - } - } - - private void handleCommandForward() { - + player.getConnection().write(response); + }, player.getConnection().eventLoop()).exceptionally((ex) -> { + logger.error("Exception while finishing regular tab completion, with request {} and response{}", request, + response, ex); + return null; + }); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 0dcac9da..9570b651 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -65,11 +65,14 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; -import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder; import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; -import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.ChatType; +import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.server.VelocityRegisteredServer; +import com.velocitypowered.proxy.tablist.InternalTabList; +import com.velocitypowered.proxy.tablist.KeyedVelocityTabList; import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; @@ -145,7 +148,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private @Nullable ModInfo modInfo; private Component playerListHeader = Component.empty(); private Component playerListFooter = Component.empty(); - private final VelocityTabList tabList; + private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; private final Collection knownChannels; @@ -166,7 +169,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; private @Nullable IdentifiedKey playerKey; - private ChatQueue chatQueue; + private final ChatQueue chatQueue; + private final ChatBuilderFactory chatBuilderFactory; ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, @Nullable InetSocketAddress virtualHost, boolean onlineMode, @Nullable IdentifiedKey playerKey) { @@ -179,16 +183,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS); this.onlineMode = onlineMode; - if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { - this.tabList = new VelocityTabList(this, server); + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + this.tabList = new VelocityTabList(this); + } else if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.tabList = new KeyedVelocityTabList(this, server); } else { this.tabList = new VelocityTabListLegacy(this, server); } this.playerKey = playerKey; this.chatQueue = new ChatQueue(this); + this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion()); } - ChatQueue getChatQueue() { + public ChatBuilderFactory getChatBuilderFactory() { + return chatBuilderFactory; + } + + public ChatQueue getChatQueue() { return chatQueue; } @@ -327,7 +338,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, public void sendMessage(@NonNull Identity identity, @NonNull Component message) { Component translated = translateMessage(message); - connection.write(ChatBuilder.builder(this.getProtocolVersion()) + connection.write(getChatBuilderFactory().builder() .component(translated).forIdentity(identity).toClient()); } @@ -339,9 +350,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, Component translated = translateMessage(message); - connection.write(ChatBuilder.builder(this.getProtocolVersion()) + connection.write(getChatBuilderFactory().builder() .component(translated).forIdentity(identity) - .setType(type == MessageType.CHAT ? ChatBuilder.ChatType.CHAT : ChatBuilder.ChatType.SYSTEM) + .setType(type == MessageType.CHAT ? ChatType.CHAT : ChatType.SYSTEM) .toClient()); } @@ -525,7 +536,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } @Override - public VelocityTabList getTabList() { + public InternalTabList getTabList() { return tabList; } @@ -913,13 +924,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, "input cannot be greater than " + LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH + " characters in length"); if (getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { - this.chatQueue.hijack(ChatBuilder.builder(getProtocolVersion()).asPlayer(this).message(input), + this.chatQueue.hijack(getChatBuilderFactory().builder().asPlayer(this).message(input), (instant, item) -> { - item.timestamp(instant); + item.setTimestamp(instant); return item.toServer(); }); } else { - ensureBackendConnection().write(ChatBuilder.builder(getProtocolVersion()) + ensureBackendConnection().write(getChatBuilderFactory().builder() .asPlayer(this).message(input).toServer()); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index 40292c78..c070d662 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -25,15 +25,11 @@ import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId; import com.google.common.base.Preconditions; import com.google.common.primitives.Longs; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.stream.JsonReader; import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.api.util.UuidUtils; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; @@ -50,7 +46,6 @@ import java.security.KeyPair; import java.security.MessageDigest; import java.util.Arrays; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadLocalRandom; import net.kyori.adventure.text.Component; @@ -60,7 +55,6 @@ import org.apache.logging.log4j.Logger; import org.asynchttpclient.ListenableFuture; import org.asynchttpclient.Response; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; public class InitialLoginSessionHandler implements MinecraftSessionHandler { @@ -111,7 +105,8 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { return true; } } else if (mcConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 - && forceKeyAuthentication) { + && forceKeyAuthentication + && mcConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) { inbound.disconnect(Component.translatable("multiplayer.disconnect.missing_public_key")); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatMessage.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatMessage.java deleted file mode 100644 index fd061194..00000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatMessage.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2018 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.crypto; - -import com.google.common.base.Preconditions; -import com.velocitypowered.api.proxy.crypto.SignedMessage; -import com.velocitypowered.proxy.util.except.QuietDecoderException; -import java.security.PublicKey; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.TemporalAmount; -import java.util.UUID; -import org.checkerframework.checker.nullness.qual.Nullable; - -public class SignedChatMessage implements SignedMessage { - - private static final QuietDecoderException INVALID_SIGNED_CHAT = - new QuietDecoderException("Couldn't parse chat message"); - - public static final TemporalAmount EXPIRY_TIME = Duration.ofMinutes(2L); - - private final String message; - private final PublicKey signer; - private final byte[] signature; - private final Instant expiry; - private final byte[] salt; - private final UUID sender; - //private final boolean isValid; - private final SignaturePair[] previousSignatures; - private final @Nullable SignaturePair previousSignature; - private final boolean isPreviewSigned; - - /** - * Create a signed message from data. - */ - public SignedChatMessage(String message, PublicKey signer, UUID sender, - Instant expiry, byte[] signature, byte[] salt, - boolean isPreviewSigned, @Nullable SignaturePair[] previousSignatures, - @Nullable SignaturePair previousSignature) { - this.message = Preconditions.checkNotNull(message); - this.signer = Preconditions.checkNotNull(signer); - this.sender = Preconditions.checkNotNull(sender); - this.signature = Preconditions.checkNotNull(signature); - this.expiry = Preconditions.checkNotNull(expiry); - this.salt = Preconditions.checkNotNull(salt); - this.isPreviewSigned = isPreviewSigned; - this.previousSignatures = previousSignatures; - this.previousSignature = previousSignature; - - //this.isValid = EncryptionUtils.verifySignature(EncryptionUtils.SHA1_WITH_RSA, signer, - // signature, salt, EncryptionUtils.longToBigEndianByteArray( - // sender.getMostSignificantBits(), sender.getLeastSignificantBits() - // ), Longs.toByteArray(expiry.getEpochSecond()), message.getBytes(StandardCharsets.UTF_8)); - } - - @Override - public PublicKey getSigner() { - return signer; - } - - @Override - public Instant getExpiryTemporal() { - return expiry; - } - - @Override - public @Nullable byte[] getSignature() { - return signature; - } - - public SignaturePair[] getPreviousSignatures() { - return previousSignatures; - } - - public SignaturePair getPreviousSignature() { - return previousSignature; - } - - //@Override - //public boolean isSignatureValid() { - // return isValid; - //} - - @Override - public String getMessage() { - return message; - } - - @Override - public UUID getSignerUuid() { - return sender; - } - - @Override - public boolean isPreviewSigned() { - return isPreviewSigned; - } - - @Override - public byte[] getSalt() { - return salt; - } - -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 7ccc0fe1..574555e5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -31,6 +31,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -50,10 +51,11 @@ import com.velocitypowered.proxy.protocol.packet.Handshake; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem; import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; @@ -66,11 +68,14 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; -import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletion; -import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -124,19 +129,24 @@ public enum StateRegistry { map(0x05, MINECRAFT_1_13, false), map(0x06, MINECRAFT_1_14, false), map(0x08, MINECRAFT_1_19, false), - map(0x09, MINECRAFT_1_19_1, false)); + map(0x09, MINECRAFT_1_19_1, false), + map(0x08, MINECRAFT_1_19_3, false)); serverbound.register(LegacyChat.class, LegacyChat::new, map(0x01, MINECRAFT_1_7_2, false), map(0x02, MINECRAFT_1_9, false), map(0x03, MINECRAFT_1_12, false), map(0x02, MINECRAFT_1_12_1, false), map(0x03, MINECRAFT_1_14, MINECRAFT_1_18_2, false)); - serverbound.register(PlayerCommand.class, PlayerCommand::new, + serverbound.register(KeyedPlayerCommand.class, KeyedPlayerCommand::new, map(0x03, MINECRAFT_1_19, false), - map(0x04, MINECRAFT_1_19_1, false)); - serverbound.register(PlayerChat.class, PlayerChat::new, + map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); + serverbound.register(KeyedPlayerChat.class, KeyedPlayerChat::new, map(0x04, MINECRAFT_1_19, false), - map(0x05, MINECRAFT_1_19_1, false)); + map(0x05, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); + serverbound.register(SessionPlayerCommand.class, SessionPlayerCommand::new, + map(0x04, MINECRAFT_1_19_3, false)); + serverbound.register(SessionPlayerChat.class, SessionPlayerChat::new, + map(0x05, MINECRAFT_1_19_3, false)); serverbound.register(ClientSettings.class, ClientSettings::new, map(0x15, MINECRAFT_1_7_2, false), map(0x04, MINECRAFT_1_9, false), @@ -144,7 +154,8 @@ public enum StateRegistry { map(0x04, MINECRAFT_1_12_1, false), map(0x05, MINECRAFT_1_14, false), map(0x07, MINECRAFT_1_19, false), - map(0x08, MINECRAFT_1_19_1, false)); + map(0x08, MINECRAFT_1_19_1, false), + map(0x07, MINECRAFT_1_19_3, false)); serverbound.register(PluginMessage.class, PluginMessage::new, map(0x17, MINECRAFT_1_7_2, false), map(0x09, MINECRAFT_1_9, false), @@ -154,7 +165,8 @@ public enum StateRegistry { map(0x0B, MINECRAFT_1_14, false), map(0x0A, MINECRAFT_1_17, false), map(0x0C, MINECRAFT_1_19, false), - map(0x0D, MINECRAFT_1_19_1, false)); + map(0x0D, MINECRAFT_1_19_1, false), + map(0x0C, MINECRAFT_1_19_3, false)); serverbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), @@ -165,7 +177,8 @@ public enum StateRegistry { map(0x10, MINECRAFT_1_16, false), map(0x0F, MINECRAFT_1_17, false), map(0x11, MINECRAFT_1_19, false), - map(0x12, MINECRAFT_1_19_1, false)); + map(0x12, MINECRAFT_1_19_1, false), + map(0x11, MINECRAFT_1_19_3, false)); serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, map(0x19, MINECRAFT_1_8, false), map(0x16, MINECRAFT_1_9, false), @@ -198,14 +211,16 @@ public enum StateRegistry { map(0x10, MINECRAFT_1_16, false), map(0x0F, MINECRAFT_1_16_2, false), map(0x11, MINECRAFT_1_17, false), - map(0x0E, MINECRAFT_1_19, false)); + map(0x0E, MINECRAFT_1_19, false), + map(0x0D, MINECRAFT_1_19_3, false)); clientbound.register(AvailableCommands.class, AvailableCommands::new, map(0x11, MINECRAFT_1_13, false), map(0x12, MINECRAFT_1_15, false), map(0x11, MINECRAFT_1_16, false), map(0x10, MINECRAFT_1_16_2, false), map(0x12, MINECRAFT_1_17, false), - map(0x0F, MINECRAFT_1_19, false)); + map(0x0F, MINECRAFT_1_19, false), + map(0x0E, MINECRAFT_1_19_3, false)); clientbound.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), @@ -216,7 +231,8 @@ public enum StateRegistry { map(0x17, MINECRAFT_1_16_2, false), map(0x18, MINECRAFT_1_17, false), map(0x15, MINECRAFT_1_19, false), - map(0x16, MINECRAFT_1_19_1, false)); + map(0x16, MINECRAFT_1_19_1, false), + map(0x15, MINECRAFT_1_19_3, false)); clientbound.register(Disconnect.class, Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), @@ -227,7 +243,8 @@ public enum StateRegistry { map(0x19, MINECRAFT_1_16_2, false), map(0x1A, MINECRAFT_1_17, false), map(0x17, MINECRAFT_1_19, false), - map(0x19, MINECRAFT_1_19_1, false)); + map(0x19, MINECRAFT_1_19_1, false), + map(0x17, MINECRAFT_1_19_3, false)); clientbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), @@ -238,7 +255,8 @@ public enum StateRegistry { map(0x1F, MINECRAFT_1_16_2, false), map(0x21, MINECRAFT_1_17, false), map(0x1E, MINECRAFT_1_19, false), - map(0x20, MINECRAFT_1_19_1, false)); + map(0x20, MINECRAFT_1_19_1, false), + map(0x1F, MINECRAFT_1_19_3, false)); clientbound.register(JoinGame.class, JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), @@ -249,7 +267,8 @@ public enum StateRegistry { map(0x24, MINECRAFT_1_16_2, false), map(0x26, MINECRAFT_1_17, false), map(0x23, MINECRAFT_1_19, false), - map(0x25, MINECRAFT_1_19_1, false)); + map(0x25, MINECRAFT_1_19_1, false), + map(0x24, MINECRAFT_1_19_3, false)); clientbound.register(Respawn.class, Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), @@ -262,7 +281,8 @@ public enum StateRegistry { map(0x39, MINECRAFT_1_16_2, true), map(0x3D, MINECRAFT_1_17, true), map(0x3B, MINECRAFT_1_19, true), - map(0x3E, MINECRAFT_1_19_1, true)); + map(0x3E, MINECRAFT_1_19_1, true), + map(0x3D, MINECRAFT_1_19_3, true)); clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, false), map(0x32, MINECRAFT_1_9, false), @@ -275,7 +295,8 @@ public enum StateRegistry { map(0x38, MINECRAFT_1_16_2, false), map(0x3C, MINECRAFT_1_17, false), map(0x3A, MINECRAFT_1_19, false), - map(0x3D, MINECRAFT_1_19_1, false)); + map(0x3D, MINECRAFT_1_19_1, false), + map(0x3C, MINECRAFT_1_19_3, false)); clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), @@ -289,7 +310,8 @@ public enum StateRegistry { map(0x5E, MINECRAFT_1_17, true), map(0x5F, MINECRAFT_1_18, true), map(0x60, MINECRAFT_1_19, true), - map(0x63, MINECRAFT_1_19_1, true)); + map(0x63, MINECRAFT_1_19_1, true), + map(0x61, MINECRAFT_1_19_3, true)); clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), @@ -302,23 +324,28 @@ public enum StateRegistry { clientbound.register(TitleSubtitlePacket.class, TitleSubtitlePacket::new, map(0x57, MINECRAFT_1_17, true), map(0x58, MINECRAFT_1_18, true), - map(0x5B, MINECRAFT_1_19_1, true)); + map(0x5B, MINECRAFT_1_19_1, true), + map(0x59, MINECRAFT_1_19_3, true)); clientbound.register(TitleTextPacket.class, TitleTextPacket::new, map(0x59, MINECRAFT_1_17, true), map(0x5A, MINECRAFT_1_18, true), - map(0x5D, MINECRAFT_1_19_1, true)); + map(0x5D, MINECRAFT_1_19_1, true), + map(0x5B, MINECRAFT_1_19_3, true)); clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new, map(0x41, MINECRAFT_1_17, true), map(0x40, MINECRAFT_1_19, true), - map(0x43, MINECRAFT_1_19_1, true)); + map(0x43, MINECRAFT_1_19_1, true), + map(0x42, MINECRAFT_1_19_3, true)); clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new, map(0x5A, MINECRAFT_1_17, true), map(0x5B, MINECRAFT_1_18, true), - map(0x5E, MINECRAFT_1_19_1, true)); + map(0x5E, MINECRAFT_1_19_1, true), + map(0x5A, MINECRAFT_1_19_3, true)); clientbound.register(TitleClearPacket.class, TitleClearPacket::new, map(0x10, MINECRAFT_1_17, true), - map(0x0D, MINECRAFT_1_19, true)); - clientbound.register(PlayerListItem.class, PlayerListItem::new, + map(0x0D, MINECRAFT_1_19, true), + map(0x0C, MINECRAFT_1_19_3, true)); + clientbound.register(LegacyPlayerListItem.class, LegacyPlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), map(0x2E, MINECRAFT_1_12_1, false), @@ -329,15 +356,22 @@ public enum StateRegistry { map(0x32, MINECRAFT_1_16_2, false), map(0x36, MINECRAFT_1_17, false), map(0x34, MINECRAFT_1_19, false), - map(0x37, MINECRAFT_1_19_1, false)); + map(0x37, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); + clientbound.register(RemovePlayerInfo.class, RemovePlayerInfo::new, + map(0x35, MINECRAFT_1_19_3, false)); + clientbound.register(UpsertPlayerInfo.class, UpsertPlayerInfo::new, + map(0x36, MINECRAFT_1_19_3, false)); clientbound.register(SystemChat.class, SystemChat::new, map(0x5F, MINECRAFT_1_19, true), - map(0x62, MINECRAFT_1_19_1, true)); + map(0x62, MINECRAFT_1_19_1, true), + map(0x60, MINECRAFT_1_19_3, true)); clientbound.register(PlayerChatCompletion.class, PlayerChatCompletion::new, - StateRegistry.map(0x15, MINECRAFT_1_19_1, true)); + map(0x15, MINECRAFT_1_19_1, true), + map(0x14, MINECRAFT_1_19_3, true)); clientbound.register(ServerData.class, ServerData::new, map(0x3F, MINECRAFT_1_19, false), - map(0x42, MINECRAFT_1_19_1, false)); + map(0x42, MINECRAFT_1_19_1, false), + map(0x41, MINECRAFT_1_19_3, false)); } }, LOGIN { @@ -397,7 +431,7 @@ public enum StateRegistry { }

void register(Class

clazz, Supplier

packetSupplier, - PacketMapping... mappings) { + PacketMapping... mappings) { if (mappings.length == 0) { throw new IllegalArgumentException("At least one mapping must be provided."); } @@ -414,11 +448,11 @@ public enum StateRegistry { } if (from.compareTo(lastValid) > 0) { throw new IllegalArgumentException( - "Last mapping version cannot be higher than highest mapping version"); + "Last mapping version cannot be higher than highest mapping version"); } } ProtocolVersion to = current == next ? lastValid != null - ? lastValid : getLast(SUPPORTED_VERSIONS) : next.protocolVersion; + ? lastValid : getLast(SUPPORTED_VERSIONS) : next.protocolVersion; ProtocolVersion lastInList = lastValid != null ? lastValid : getLast(SUPPORTED_VERSIONS); @@ -563,14 +597,14 @@ public enum StateRegistry { /** * Creates a PacketMapping using the provided arguments. * - * @param id Packet Id - * @param version Protocol version - * @param encodeOnly When true packet decoding will be disabled + * @param id Packet Id + * @param version Protocol version + * @param encodeOnly When true packet decoding will be disabled * @param lastValidProtocolVersion Last version this Mapping is valid at * @return PacketMapping with the provided arguments */ private static PacketMapping map(int id, ProtocolVersion version, - ProtocolVersion lastValidProtocolVersion, boolean encodeOnly) { + ProtocolVersion lastValidProtocolVersion, boolean encodeOnly) { return new PacketMapping(id, version, lastValidProtocolVersion, encodeOnly); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java index 7a19b496..8bb4f69c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java @@ -67,6 +67,7 @@ public class EncryptionResponse implements MinecraftPacket { this.sharedSecret = ProtocolUtils.readByteArray(buf, 128); if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 + && version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0 && !buf.readBoolean()) { salt = buf.readLong(); } @@ -83,7 +84,8 @@ public class EncryptionResponse implements MinecraftPacket { public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { ProtocolUtils.writeByteArray(buf, sharedSecret); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 + && version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) { if (salt != null) { buf.writeBoolean(false); buf.writeLong(salt); @@ -108,6 +110,9 @@ public class EncryptionResponse implements MinecraftPacket { // It turns out these come out to the same length, whether we're talking >=1.8 or not. // The length prefix always winds up being 2 bytes. int base = 256 + 2 + 2; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + return base + 128; + } if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { // Verify token is twice as long on 1.19+ // Additional 1 byte for left <> right and 8 bytes for salt diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPlayerListItem.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPlayerListItem.java index 9f490824..0269a78d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPlayerListItem.java @@ -33,7 +33,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.checkerframework.checker.nullness.qual.Nullable; -public class PlayerListItem implements MinecraftPacket { +public class LegacyPlayerListItem implements MinecraftPacket { public static final int ADD_PLAYER = 0; public static final int UPDATE_GAMEMODE = 1; @@ -43,12 +43,12 @@ public class PlayerListItem implements MinecraftPacket { private int action; private final List items = new ArrayList<>(); - public PlayerListItem(int action, List items) { + public LegacyPlayerListItem(int action, List items) { this.action = action; this.items.addAll(items); } - public PlayerListItem() { + public LegacyPlayerListItem() { } public int getAction() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerChatPreview.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemovePlayerInfo.java similarity index 55% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerChatPreview.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemovePlayerInfo.java index f97410e5..5827215b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerChatPreview.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemovePlayerInfo.java @@ -15,46 +15,52 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet.chat; +package com.velocitypowered.proxy.protocol.packet; +import com.google.common.collect.Lists; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; -public class ServerChatPreview implements MinecraftPacket { +public class RemovePlayerInfo implements MinecraftPacket { + private Collection profilesToRemove; - private int id; - private @Nullable Component preview; - - public Component getPreview() { - return preview; + public RemovePlayerInfo() { + this.profilesToRemove = new ArrayList<>(); } - public int getId() { - return id; + public RemovePlayerInfo(Collection profilesToRemove) { + this.profilesToRemove = profilesToRemove; + } + + public Collection getProfilesToRemove() { + return profilesToRemove; + } + + public void setProfilesToRemove(Collection profilesToRemove) { + this.profilesToRemove = profilesToRemove; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - id = buf.readInt(); - if (buf.readBoolean()) { - preview = GsonComponentSerializer.gson().deserialize(ProtocolUtils.readString(buf)); + int length = ProtocolUtils.readVarInt(buf); + Collection profilesToRemove = Lists.newArrayListWithCapacity(length); + for (int idx = 0; idx < length; idx++) { + profilesToRemove.add(ProtocolUtils.readUuid(buf)); } + this.profilesToRemove = profilesToRemove; } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - buf.writeInt(id); - if (preview != null) { - buf.writeBoolean(true); - ProtocolUtils.writeString(buf, GsonComponentSerializer.gson().serialize(preview)); - } else { - buf.writeBoolean(false); + ProtocolUtils.writeVarInt(buf, this.profilesToRemove.size()); + for (UUID uuid : this.profilesToRemove) { + ProtocolUtils.writeUuid(buf, uuid); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java index fc2d42e0..7126c8c1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java @@ -36,7 +36,7 @@ public class Respawn implements MinecraftPacket { private short difficulty; private short gamemode; private String levelType = ""; - private boolean shouldKeepPlayerData; // 1.16+ + private byte dataToKeep; // 1.16+ private DimensionInfo dimensionInfo; // 1.16-1.16.1 private short previousGamemode; // 1.16+ private DimensionData currentDimensionData; // 1.16.2+ @@ -46,14 +46,15 @@ public class Respawn implements MinecraftPacket { } public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode, - String levelType, boolean shouldKeepPlayerData, DimensionInfo dimensionInfo, - short previousGamemode, DimensionData currentDimensionData, @Nullable Pair lastDeathPosition) { + String levelType, byte dataToKeep, DimensionInfo dimensionInfo, + short previousGamemode, DimensionData currentDimensionData, + @Nullable Pair lastDeathPosition) { this.dimension = dimension; this.partialHashedSeed = partialHashedSeed; this.difficulty = difficulty; this.gamemode = gamemode; this.levelType = levelType; - this.shouldKeepPlayerData = shouldKeepPlayerData; + this.dataToKeep = dataToKeep; this.dimensionInfo = dimensionInfo; this.previousGamemode = previousGamemode; this.currentDimensionData = currentDimensionData; @@ -63,7 +64,7 @@ public class Respawn implements MinecraftPacket { public static Respawn fromJoinGame(JoinGame joinGame) { return new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), - false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), + (byte) 0, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition()); } @@ -107,12 +108,12 @@ public class Respawn implements MinecraftPacket { this.levelType = levelType; } - public boolean getShouldKeepPlayerData() { - return shouldKeepPlayerData; + public byte getDataToKeep() { + return dataToKeep; } - public void setShouldKeepPlayerData(boolean shouldKeepPlayerData) { - this.shouldKeepPlayerData = shouldKeepPlayerData; + public void setDataToKeep(byte dataToKeep) { + this.dataToKeep = dataToKeep; } public short getPreviousGamemode() { @@ -139,7 +140,7 @@ public class Respawn implements MinecraftPacket { + ", difficulty=" + difficulty + ", gamemode=" + gamemode + ", levelType='" + levelType + '\'' - + ", shouldKeepPlayerData=" + shouldKeepPlayerData + + ", dataToKeep=" + dataToKeep + ", dimensionRegistryName='" + dimensionInfo.toString() + '\'' + ", dimensionInfo=" + dimensionInfo + ", previousGamemode=" + previousGamemode @@ -153,7 +154,7 @@ public class Respawn implements MinecraftPacket { String levelName = null; if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0 - && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { + && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader()); dimensionIdentifier = ProtocolUtils.readString(buf); this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version) @@ -177,7 +178,13 @@ public class Respawn implements MinecraftPacket { boolean isDebug = buf.readBoolean(); boolean isFlat = buf.readBoolean(); this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); - this.shouldKeepPlayerData = buf.readBoolean(); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + this.dataToKeep = buf.readByte(); + } else if (buf.readBoolean()) { + this.dataToKeep = 1; + } else { + this.dataToKeep = 0; + } } else { this.levelType = ProtocolUtils.readString(buf, 16); } @@ -190,7 +197,7 @@ public class Respawn implements MinecraftPacket { public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0 - && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { + && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails()); ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); } else { @@ -211,7 +218,11 @@ public class Respawn implements MinecraftPacket { buf.writeByte(previousGamemode); buf.writeBoolean(dimensionInfo.isDebugType()); buf.writeBoolean(dimensionInfo.isFlat()); - buf.writeBoolean(shouldKeepPlayerData); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + buf.writeByte(dataToKeep); + } else { + buf.writeBoolean(dataToKeep != 0); + } } else { ProtocolUtils.writeString(buf, levelType); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java index 102fb746..5b654e80 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java @@ -30,17 +30,15 @@ public class ServerData implements MinecraftPacket { private @Nullable Component description; private @Nullable Favicon favicon; - private boolean previewsChat; private boolean secureChatEnforced; // Added in 1.19.1 public ServerData() { } public ServerData(@Nullable Component description, @Nullable Favicon favicon, - boolean previewsChat, boolean secureChatEnforced) { + boolean secureChatEnforced) { this.description = description; this.favicon = favicon; - this.previewsChat = previewsChat; this.secureChatEnforced = secureChatEnforced; } @@ -54,7 +52,9 @@ public class ServerData implements MinecraftPacket { if (buf.readBoolean()) { this.favicon = new Favicon(ProtocolUtils.readString(buf)); } - this.previewsChat = buf.readBoolean(); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) { + buf.readBoolean(); + } if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { this.secureChatEnforced = buf.readBoolean(); } @@ -77,7 +77,9 @@ public class ServerData implements MinecraftPacket { ProtocolUtils.writeString(buf, favicon.getBase64Url()); } - buf.writeBoolean(this.previewsChat); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) { + buf.writeBoolean(false); + } if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { buf.writeBoolean(this.secureChatEnforced); } @@ -88,18 +90,14 @@ public class ServerData implements MinecraftPacket { return handler.handle(this); } - public Component getDescription() { + public @Nullable Component getDescription() { return description; } - public Favicon getFavicon() { + public @Nullable Favicon getFavicon() { return favicon; } - public boolean isPreviewsChat() { - return previewsChat; - } - public boolean isSecureChatEnforced() { return secureChatEnforced; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java index de57bf91..ee98d8d9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java @@ -35,7 +35,7 @@ public class ServerLogin implements MinecraftPacket { private static final QuietDecoderException EMPTY_USERNAME = new QuietDecoderException("Empty username!"); private @Nullable String username; - private @Nullable IdentifiedKey playerKey; // Introduced in 1.19 + private @Nullable IdentifiedKey playerKey; // Introduced in 1.19.3 private @Nullable UUID holderUuid; // Used for key revision 2 public ServerLogin() { @@ -46,6 +46,12 @@ public class ServerLogin implements MinecraftPacket { this.playerKey = playerKey; } + public ServerLogin(String username, @Nullable UUID holderUuid) { + this.username = Preconditions.checkNotNull(username, "username"); + this.holderUuid = holderUuid; + this.playerKey = null; + } + public String getUsername() { if (username == null) { throw new IllegalStateException("No username found!"); @@ -53,15 +59,15 @@ public class ServerLogin implements MinecraftPacket { return username; } - public IdentifiedKey getPlayerKey() { - return playerKey; + public @Nullable IdentifiedKey getPlayerKey() { + return this.playerKey; } public void setPlayerKey(IdentifiedKey playerKey) { this.playerKey = playerKey; } - public UUID getHolderUuid() { + public @Nullable UUID getHolderUuid() { return holderUuid; } @@ -74,15 +80,21 @@ public class ServerLogin implements MinecraftPacket { } @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + public void decode(ByteBuf buf, Direction direction, ProtocolVersion version) { username = ProtocolUtils.readString(buf, 16); if (username.isEmpty()) { throw EMPTY_USERNAME; } if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { - if (buf.readBoolean()) { - playerKey = ProtocolUtils.readPlayerKey(version, buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + playerKey = null; + } else { + if (buf.readBoolean()) { + playerKey = ProtocolUtils.readPlayerKey(version, buf); + } else { + playerKey = null; + } } if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { @@ -90,6 +102,8 @@ public class ServerLogin implements MinecraftPacket { holderUuid = ProtocolUtils.readUuid(buf); } } + } else { + playerKey = null; } } @@ -101,17 +115,22 @@ public class ServerLogin implements MinecraftPacket { ProtocolUtils.writeString(buf, username); if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { - if (playerKey != null) { - buf.writeBoolean(true); - ProtocolUtils.writePlayerKey(buf, playerKey); - } else { - buf.writeBoolean(false); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) { + if (playerKey != null) { + buf.writeBoolean(true); + ProtocolUtils.writePlayerKey(buf, playerKey); + } else { + buf.writeBoolean(false); + } } if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (playerKey != null && playerKey.getSignatureHolder() != null) { buf.writeBoolean(true); ProtocolUtils.writeUuid(buf, playerKey.getSignatureHolder()); + } else if (this.holderUuid != null) { + buf.writeBoolean(true); + ProtocolUtils.writeUuid(buf, this.holderUuid); } else { buf.writeBoolean(false); } @@ -126,13 +145,15 @@ public class ServerLogin implements MinecraftPacket { int base = 1 + (16 * 4); // Adjustments for Key-authentication if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { - // + 1 for the boolean present/ not present - // + 8 for the long expiry - // + 2 len for varint key size - // + 294 for the key - // + 2 len for varint signature size - // + 512 for signature - base += 1 + 8 + 2 + 294 + 2 + 512; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) { + // + 1 for the boolean present/ not present + // + 8 for the long expiry + // + 2 len for varint key size + // + 294 for the key + // + 2 len for varint signature size + // + 512 for signature + base += 1 + 8 + 2 + 294 + 2 + 512; + } if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { // +1 boolean uuid optional // + 2 * 8 for the long msb/lsb diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfo.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfo.java new file mode 100644 index 00000000..765aab2c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfo.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; +import io.netty.buffer.ByteBuf; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +public class UpsertPlayerInfo implements MinecraftPacket { + private final EnumSet actions; + private final List entries; + + public UpsertPlayerInfo() { + this.actions = EnumSet.noneOf(Action.class); + this.entries = new ArrayList<>(); + } + + public UpsertPlayerInfo(Action action) { + this.actions = EnumSet.of(action); + this.entries = new ArrayList<>(); + } + + public UpsertPlayerInfo(EnumSet actions, List entries) { + this.actions = actions; + this.entries = entries; + } + + public List getEntries() { + return entries; + } + + public EnumSet getActions() { + return actions; + } + + public boolean containsAction(Action action) { + return this.actions.contains(action); + } + + public void addAction(Action action) { + this.actions.add(action); + } + + public void addAllActions(Collection actions) { + this.actions.addAll(actions); + } + + public void addEntry(Entry entry) { + this.entries.add(entry); + } + + public void addAllEntries(Collection entries) { + this.entries.addAll(entries); + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + Action[] actions = Action.class.getEnumConstants(); + byte[] bytes = new byte[-Math.floorDiv(-actions.length, 8)]; + buf.readBytes(bytes); + BitSet actionSet = BitSet.valueOf(bytes); + + for (int idx = 0; idx < actions.length; idx++) { + if (actionSet.get(idx)) { + addAction(actions[idx]); + } + } + + int length = ProtocolUtils.readVarInt(buf); + for (int idx = 0; idx < length; idx++) { + Entry entry = new Entry(ProtocolUtils.readUuid(buf)); + for (Action action : this.actions) { + action.read.read(protocolVersion, buf, entry); + } + addEntry(entry); + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + Action[] actions = Action.class.getEnumConstants(); + BitSet set = new BitSet(actions.length); + for (int idx = 0; idx < actions.length; idx++) { + set.set(idx, this.actions.contains(actions[idx])); + } + + byte[] bytes = set.toByteArray(); + buf.writeBytes(Arrays.copyOf(bytes, -Math.floorDiv(-actions.length, 8))); + + ProtocolUtils.writeVarInt(buf, this.entries.size()); + for (Entry entry : this.entries) { + ProtocolUtils.writeUuid(buf, entry.profileId); + + for (Action action : this.actions) { + action.write.write(protocolVersion, buf, entry); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + public BitSet readFixedBitSet(ByteBuf buf, int param0) { + byte[] var0 = new byte[-Math.floorDiv(-param0, 8)]; + buf.readBytes(var0); + return BitSet.valueOf(var0); + } + + public enum Action { + ADD_PLAYER((ignored, buf, info) -> { // read + info.profile = new GameProfile( + info.profileId, + ProtocolUtils.readString(buf, 16), + ProtocolUtils.readProperties(buf) + ); + }, (ignored, buf, info) -> { // write + ProtocolUtils.writeString(buf, info.profile.getName()); + ProtocolUtils.writeProperties(buf, info.profile.getProperties()); + }), + INITIALIZE_CHAT((version, buf, info) -> { // read + if (buf.readBoolean()) { + info.chatSession = new RemoteChatSession(version, buf); + } else { + info.chatSession = null; + } + }, (ignored, buf, info) -> { // write + buf.writeBoolean(info.chatSession != null); + if (info.chatSession != null) { + info.chatSession.write(buf); + } + }), + UPDATE_GAME_MODE((ignored, buf, info) -> { // read + info.gameMode = ProtocolUtils.readVarInt(buf); + }, (ignored, buf, info) -> { // write + ProtocolUtils.writeVarInt(buf, info.gameMode); + }), + UPDATE_LISTED((ignored, buf, info) -> { // read + info.listed = buf.readBoolean(); + }, (ignored, buf, info) -> { // write + buf.writeBoolean(info.listed); + }), + UPDATE_LATENCY((ignored, buf, info) -> { // read + info.latency = ProtocolUtils.readVarInt(buf); + }, (ignored, buf, info) -> { // write + ProtocolUtils.writeVarInt(buf, info.latency); + }), + UPDATE_DISPLAY_NAME((version, buf, info) -> { // read + if (buf.readBoolean()) { + info.displayName = ProtocolUtils.getJsonChatSerializer(version) + .deserialize(ProtocolUtils.readString(buf)); + } else { + info.displayName = null; + } + }, (version, buf, info) -> { // write + buf.writeBoolean(info.displayName != null); + if (info.displayName != null) { + ProtocolUtils.writeString(buf, ProtocolUtils.getJsonChatSerializer(version) + .serialize(info.displayName)); + } + }); + + private final Read read; + private final Write write; + + Action(Read read, Write write) { + this.read = read; + this.write = write; + } + + private interface Read { + void read(ProtocolVersion version, ByteBuf buf, Entry info); + } + + private interface Write { + void write(ProtocolVersion version, ByteBuf buf, Entry info); + } + } + + public static class Entry { + private final UUID profileId; + private GameProfile profile; + private boolean listed; + private int latency; + private int gameMode; + @Nullable + private Component displayName; + @Nullable + private RemoteChatSession chatSession; + + public Entry(UUID uuid) { + this.profileId = uuid; + } + + public UUID getProfileId() { + return profileId; + } + + public GameProfile getProfile() { + return profile; + } + + public boolean isListed() { + return listed; + } + + public int getLatency() { + return latency; + } + + public int getGameMode() { + return gameMode; + } + + @Nullable + public Component getDisplayName() { + return displayName; + } + + @Nullable + public RemoteChatSession getChatSession() { + return chatSession; + } + + public void setProfile(GameProfile profile) { + this.profile = profile; + } + + public void setListed(boolean listed) { + this.listed = listed; + } + + public void setLatency(int latency) { + this.latency = latency; + } + + public void setGameMode(int gameMode) { + this.gameMode = gameMode; + } + + public void setDisplayName(@Nullable Component displayName) { + this.displayName = displayName; + } + + public void setChatSession(@Nullable RemoteChatSession chatSession) { + this.chatSession = chatSession; + } + + @Override + public String toString() { + return "Entry{" + + "profileId=" + profileId + + ", profile=" + profile + + ", listed=" + listed + + ", latency=" + latency + + ", gameMode=" + gameMode + + ", displayName=" + displayName + + ", chatSession=" + chatSession + + '}'; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java index d3dbd5f6..61305e8e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java @@ -47,7 +47,7 @@ public class ArgumentIdentifier { for (ProtocolVersion v : ProtocolVersion.values()) { if (v.compareTo(current.getVersion()) >= 0) { - temp.put(v, current.getId()); + temp.putIfAbsent(v, current.getId()); } } previous = current.getVersion(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java index bdadb957..54b4d100 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet; import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; @@ -48,14 +49,14 @@ public class ArgumentPropertyRegistry { } private static final Map> byIdentifier = - new HashMap<>(); + new HashMap<>(); private static final Map, ArgumentPropertySerializer> byClass = new HashMap<>(); private static final Map, ArgumentIdentifier> classToId = - new HashMap<>(); + new HashMap<>(); private static > void register(ArgumentIdentifier identifier, - Class klazz, ArgumentPropertySerializer serializer) { + Class klazz, ArgumentPropertySerializer serializer) { byIdentifier.put(identifier, serializer); byClass.put(klazz, serializer); classToId.put(klazz, identifier); @@ -72,6 +73,7 @@ public class ArgumentPropertyRegistry { /** * Deserializes the {@link ArgumentType}. + * * @param buf the buffer to deserialize * @return the deserialized {@link ArgumentType} */ @@ -93,7 +95,8 @@ public class ArgumentPropertyRegistry { /** * Serializes the {@code type} into the provided {@code buf}. - * @param buf the buffer to serialize into + * + * @param buf the buffer to serialize into * @param type the type to serialize */ public static void serialize(ByteBuf buf, ArgumentType type, @@ -122,12 +125,13 @@ public class ArgumentPropertyRegistry { /** * Writes the {@link ArgumentIdentifier} to a version-specific buffer. - * @param buf the buffer to write to - * @param identifier the identifier to write + * + * @param buf the buffer to write to + * @param identifier the identifier to write * @param protocolVersion the protocol version to use */ public static void writeIdentifier(ByteBuf buf, ArgumentIdentifier identifier, - ProtocolVersion protocolVersion) { + ProtocolVersion protocolVersion) { if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { Integer id = identifier.getIdByProtocolVersion(protocolVersion); Preconditions.checkNotNull(id, "Don't know how to serialize type " + identifier); @@ -141,7 +145,8 @@ public class ArgumentPropertyRegistry { /** * Reads the {@link ArgumentIdentifier} from a version-specific buffer. - * @param buf the buffer to write to + * + * @param buf the buffer to write to * @param protocolVersion the protocol version to use * @return the identifier read from the buffer */ @@ -214,27 +219,31 @@ public class ArgumentPropertyRegistry { empty(id("minecraft:team", mapSet(MINECRAFT_1_19, 31))); empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_19, 32))); empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_19, 33))); - empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19, 34))); - empty(id("minecraft:function", mapSet(MINECRAFT_1_19, 35))); - empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_19, 36))); - empty(id("minecraft:int_range", mapSet(MINECRAFT_1_19, 37))); - empty(id("minecraft:float_range", mapSet(MINECRAFT_1_19, 38))); - empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19, 39))); - empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19, 40))); - empty(id("minecraft:dimension", mapSet(MINECRAFT_1_19, 41))); - empty(id("minecraft:time", mapSet(MINECRAFT_1_19, 42))); // added in 1.14 + empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 34))); + empty(id("minecraft:function", mapSet(MINECRAFT_1_19_3, 34), mapSet(MINECRAFT_1_19, 35))); + empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_19_3, 35), mapSet(MINECRAFT_1_19, 36))); + empty(id("minecraft:int_range", mapSet(MINECRAFT_1_19_3, 36), mapSet(MINECRAFT_1_19, 37))); + empty(id("minecraft:float_range", mapSet(MINECRAFT_1_19_3, 37), mapSet(MINECRAFT_1_19, 38))); + empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 39))); + empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 40))); + empty(id("minecraft:dimension", mapSet(MINECRAFT_1_19_3, 38), mapSet(MINECRAFT_1_19, 41))); + empty(id("minecraft:gamemode", mapSet(MINECRAFT_1_19_3, 39))); // 1.19.3 + empty(id("minecraft:time", mapSet(MINECRAFT_1_19_3, 40), mapSet(MINECRAFT_1_19, 42))); // added in 1.14 - register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_19, 43)), - RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); - register(id("minecraft:resource", mapSet(MINECRAFT_1_19, 44)), - RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_19_3, 41), mapSet(MINECRAFT_1_19, 43)), + RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + register(id("minecraft:resource_or_tag_key", mapSet(MINECRAFT_1_19_3, 42)), + RegistryKeyArgumentList.ResourceOrTagKey.class, RegistryKeyArgumentList.ResourceOrTagKey.Serializer.REGISTRY); + register(id("minecraft:resource", mapSet(MINECRAFT_1_19_3, 43), mapSet(MINECRAFT_1_19, 44)), + RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + register(id("minecraft:resource_key", mapSet(MINECRAFT_1_19_3, 44)), + RegistryKeyArgumentList.ResourceKey.class, RegistryKeyArgumentList.ResourceKey.Serializer.REGISTRY); empty(id("minecraft:template_mirror", mapSet(MINECRAFT_1_19, 45))); // 1.19 empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_19, 46))); // 1.19 empty(id("minecraft:uuid", mapSet(MINECRAFT_1_19, 47))); // added in 1.16 - // Crossstitch support register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java new file mode 100644 index 00000000..2b5873a7 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public final class RegistryKeyArgumentList { + + public static class ResourceOrTag extends RegistryKeyArgument { + + public ResourceOrTag(String identifier) { + super(identifier); + } + + public static class Serializer implements ArgumentPropertySerializer { + static final ResourceOrTag.Serializer REGISTRY = new ResourceOrTag.Serializer(); + + @Override + public ResourceOrTag deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { + return new ResourceOrTag(ProtocolUtils.readString(buf)); + } + + @Override + public void serialize(ResourceOrTag object, ByteBuf buf, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, object.getIdentifier()); + } + } + } + + public static class ResourceOrTagKey extends RegistryKeyArgument { + + public ResourceOrTagKey(String identifier) { + super(identifier); + } + + public static class Serializer implements ArgumentPropertySerializer { + static final ResourceOrTagKey.Serializer REGISTRY = new ResourceOrTagKey.Serializer(); + + @Override + public ResourceOrTagKey deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { + return new ResourceOrTagKey(ProtocolUtils.readString(buf)); + } + + @Override + public void serialize(ResourceOrTagKey object, ByteBuf buf, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, object.getIdentifier()); + } + } + } + + public static class Resource extends RegistryKeyArgument { + + public Resource(String identifier) { + super(identifier); + } + + public static class Serializer implements ArgumentPropertySerializer { + static final Resource.Serializer REGISTRY = new Resource.Serializer(); + + @Override + public Resource deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { + return new Resource(ProtocolUtils.readString(buf)); + } + + @Override + public void serialize(Resource object, ByteBuf buf, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, object.getIdentifier()); + } + } + } + + public static class ResourceKey extends RegistryKeyArgument { + + public ResourceKey(String identifier) { + super(identifier); + } + + public static class Serializer implements ArgumentPropertySerializer { + static final ResourceKey.Serializer REGISTRY = new ResourceKey.Serializer(); + + @Override + public ResourceKey deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { + return new ResourceKey(ProtocolUtils.readString(buf)); + } + + @Override + public void serialize(ResourceKey object, ByteBuf buf, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, object.getIdentifier()); + } + } + } + + RegistryKeyArgumentList() { + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatBuilder.java deleted file mode 100644 index c71ec32f..00000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatBuilder.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2018 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet.chat; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.crypto.SignedChatCommand; -import com.velocitypowered.proxy.crypto.SignedChatMessage; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import java.time.Instant; -import java.util.UUID; -import net.kyori.adventure.identity.Identity; -import net.kyori.adventure.text.Component; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.jetbrains.annotations.NotNull; - -public class ChatBuilder { - - private final ProtocolVersion version; - - private @MonotonicNonNull Component component; - private @MonotonicNonNull String message; - private @MonotonicNonNull SignedChatMessage signedChatMessage; - private @MonotonicNonNull SignedChatCommand signedCommand; - - private @Nullable Player sender; - private @Nullable Identity senderIdentity; - private @NotNull Instant timestamp; - - private ChatType type = ChatType.CHAT; - - private ChatBuilder(ProtocolVersion version) { - this.version = version; - this.timestamp = Instant.now(); - } - - public static ChatBuilder builder(ProtocolVersion version) { - return new ChatBuilder(Preconditions.checkNotNull(version)); - } - - public ChatBuilder component(Component message) { - this.component = Preconditions.checkNotNull(message); - return this; - } - - /** - * Sets the message to the provided message. - * - * @param message The message for the chat. - * @return {@code this} - */ - public ChatBuilder message(String message) { - Preconditions.checkArgument(this.message == null); - this.message = Preconditions.checkNotNull(message); - return this; - } - - /** - * Sets the signed message to the provided message. - * - * @param message The signed message for the chat. - * @return {@code this} - */ - public ChatBuilder message(SignedChatMessage message) { - Preconditions.checkNotNull(message); - Preconditions.checkArgument(this.message == null); - this.message = message.getMessage(); - this.signedChatMessage = message; - return this; - } - - /** - * Sets the signed command to the provided command. - * - * @param command The signed command for the chat. - * @return {@code this} - */ - public ChatBuilder message(SignedChatCommand command) { - Preconditions.checkNotNull(command); - Preconditions.checkArgument(this.message == null); - this.message = command.getBaseCommand(); - this.signedCommand = command; - return this; - } - - - public ChatBuilder setType(ChatType type) { - this.type = type; - return this; - } - - public ChatBuilder asPlayer(@Nullable Player player) { - this.sender = player; - return this; - } - - public ChatBuilder forIdentity(@Nullable Identity identity) { - this.senderIdentity = identity; - return this; - } - - public ChatBuilder timestamp(Instant timestamp) { - this.timestamp = timestamp; - return this; - } - - public ChatBuilder asServer() { - this.sender = null; - return this; - } - - /** - * Creates a {@link MinecraftPacket} which can be sent to the client; using the provided information in the builder. - * - * @return The {@link MinecraftPacket} to send to the client. - */ - public MinecraftPacket toClient() { - // This is temporary - UUID identity = sender == null ? (senderIdentity == null ? Identity.nil().uuid() - : senderIdentity.uuid()) : sender.getUniqueId(); - Component msg = component == null ? Component.text(message) : component; - - if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { - // hard override chat > system for now - return new SystemChat(msg, type == ChatType.CHAT ? ChatType.SYSTEM : type); - } else { - return new LegacyChat(ProtocolUtils.getJsonChatSerializer(version).serialize(msg), type.getId(), identity); - } - } - - /** - * Creates a {@link MinecraftPacket} which can be sent to the server; using the provided information in the builder. - * - * @return The {@link MinecraftPacket} to send to the server. - */ - public MinecraftPacket toServer() { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { - if (signedChatMessage != null) { - return new PlayerChat(signedChatMessage); - } else if (signedCommand != null) { - return new PlayerCommand(signedCommand); - } else { - // Well crap - if (message.startsWith("/")) { - return new PlayerCommand(message.substring(1), ImmutableList.of(), timestamp); - } else { - // This will produce an error on the server, but needs to be here. - return new PlayerChat(message); - } - } - } - LegacyChat chat = new LegacyChat(); - chat.setMessage(message); - return chat; - } - - public static enum ChatType { - CHAT((byte) 0), - SYSTEM((byte) 1), - GAME_INFO((byte) 2); - - private final byte raw; - - ChatType(byte raw) { - this.raw = raw; - } - - public byte getId() { - return raw; - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatHandler.java new file mode 100644 index 00000000..f0ee3175 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.proxy.protocol.MinecraftPacket; + +public interface ChatHandler { + Class packetClass(); + + void handlePlayerChatInternal(T packet); + + default boolean handlePlayerChat(MinecraftPacket packet) { + if (packetClass().isInstance(packet)) { + handlePlayerChatInternal(packetClass().cast(packet)); + return true; + } + return false; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java index fc4cc00a..28b0b9fc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java @@ -23,6 +23,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import java.time.Instant; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import org.apache.logging.log4j.LogManager; /** * A precisely ordered queue which allows for outside entries into the ordered queue through piggybacking timestamps. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatTimeKeeper.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatTimeKeeper.java new file mode 100644 index 00000000..65366200 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatTimeKeeper.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import java.time.Instant; + +public class ChatTimeKeeper { + private Instant lastTimestamp; + + public ChatTimeKeeper() { + this.lastTimestamp = Instant.MIN; + } + + public boolean update(Instant instant) { + if (instant.isBefore(this.lastTimestamp)) { + this.lastTimestamp = instant; + return false; + } + this.lastTimestamp = instant; + return true; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatType.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatType.java new file mode 100644 index 00000000..f51e3b7b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatType.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +public enum ChatType { + CHAT((byte) 0), + SYSTEM((byte) 1), + GAME_INFO((byte) 2); + + private final byte raw; + + ChatType(byte raw) { + this.raw = raw; + } + + public byte getId() { + return raw; + } +} \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java new file mode 100644 index 00000000..20c295cc --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public interface CommandHandler { + Logger logger = LogManager.getLogger(CommandHandler.class); + + Class packetClass(); + + void handlePlayerCommandInternal(T packet); + + default boolean handlePlayerCommand(MinecraftPacket packet) { + if (packetClass().isInstance(packet)) { + handlePlayerCommandInternal(packetClass().cast(packet)); + return true; + } + return false; + } + + default CompletableFuture runCommand(VelocityServer server, ConnectedPlayer player, String command, + Function hasRunPacketFunction) { + return server.getCommandManager().executeImmediatelyAsync(player, command).thenApply(hasRunPacketFunction); + } + + default void queueCommandResult(VelocityServer server, ConnectedPlayer player, + Function> futurePacketCreator, + String message, Instant timestamp) { + player.getChatQueue().queuePacket( + server.getCommandManager().callCommandEvent(player, message).thenComposeAsync(futurePacketCreator) + .thenApply(pkt -> { + if (server.getConfiguration().isLogCommandExecutions()) { + logger.info("{} -> executed command /{}", player, message); + } + return pkt; + }).exceptionally(e -> { + logger.info("Exception occurred while running command for {}", player.getUsername(), e); + player.sendMessage(Component.translatable("velocity.command.generic-error", NamedTextColor.RED)); + return null; + }), timestamp); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java new file mode 100644 index 00000000..eb89601f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import java.util.Arrays; +import java.util.BitSet; + +public class LastSeenMessages { + private static final int DIV_FLOOR = -Math.floorDiv(-20, 8); + private int offset; + private BitSet acknowledged; + + public LastSeenMessages() { + this.offset = 0; + this.acknowledged = new BitSet(); + } + + public LastSeenMessages(ByteBuf buf) { + this.offset = ProtocolUtils.readVarInt(buf); + + byte[] bytes = new byte[DIV_FLOOR]; + buf.readBytes(bytes); + this.acknowledged = BitSet.valueOf(bytes); + } + + public void encode(ByteBuf buf) { + ProtocolUtils.writeVarInt(buf, offset); + buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR)); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RemoteChatSession.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RemoteChatSession.java new file mode 100644 index 00000000..fdd37726 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RemoteChatSession.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.player.ChatSession; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import java.util.Objects; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class RemoteChatSession implements ChatSession { + private final @Nullable UUID sessionId; + private final IdentifiedKey identifiedKey; + + public RemoteChatSession(ProtocolVersion version, ByteBuf buf) { + this.sessionId = ProtocolUtils.readUuid(buf); + this.identifiedKey = ProtocolUtils.readPlayerKey(version, buf); + } + + public RemoteChatSession(@Nullable UUID sessionId, IdentifiedKey identifiedKey) { + this.sessionId = sessionId; + this.identifiedKey = identifiedKey; + } + + public IdentifiedKey getIdentifiedKey() { + return identifiedKey; + } + + public @Nullable UUID getSessionId() { + return sessionId; + } + + public void write(ByteBuf buf) { + ProtocolUtils.writeUuid(buf, Objects.requireNonNull(this.sessionId)); + ProtocolUtils.writePlayerKey(buf, this.identifiedKey); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerPlayerChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerPlayerChat.java deleted file mode 100644 index eccfddfa..00000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerPlayerChat.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2018 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet.chat; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import java.time.Instant; -import java.util.UUID; -import net.kyori.adventure.text.Component; -import org.checkerframework.checker.nullness.qual.Nullable; - -public class ServerPlayerChat implements MinecraftPacket { - - private Component component; - private @Nullable Component unsignedComponent; - private int type; - - private UUID sender; - private Component senderName; - private @Nullable Component teamName; - - private Instant expiry; - - public void setType(int type) { - this.type = type; - } - - public void setComponent(Component component) { - this.component = component; - } - - public int getType() { - return type; - } - - public Component getComponent() { - return component; - } - - @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - component = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); - if (buf.readBoolean()) { - unsignedComponent = component = ProtocolUtils.getJsonChatSerializer(protocolVersion) - .deserialize(ProtocolUtils.readString(buf)); - } - - type = ProtocolUtils.readVarInt(buf); - - sender = ProtocolUtils.readUuid(buf); - senderName = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); - if (buf.readBoolean()) { - teamName = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); - } - - expiry = Instant.ofEpochMilli(buf.readLong()); - - long salt = buf.readLong(); - byte[] signature = ProtocolUtils.readByteArray(buf); - } - - @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - // TBD - } - - @Override - public boolean handle(MinecraftSessionHandler handler) { - return handler.handle(this); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java index 4d877119..237c8caf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java @@ -28,15 +28,15 @@ public class SystemChat implements MinecraftPacket { public SystemChat() {} - public SystemChat(Component component, ChatBuilder.ChatType type) { + public SystemChat(Component component, ChatType type) { this.component = component; this.type = type; } private Component component; - private ChatBuilder.ChatType type; + private ChatType type; - public ChatBuilder.ChatType getType() { + public ChatType getType() { return type; } @@ -48,7 +48,7 @@ public class SystemChat implements MinecraftPacket { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { component = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); // System chat is never decoded so this doesn't matter for now - type = ChatBuilder.ChatType.values()[ProtocolUtils.readVarInt(buf)]; + type = ChatType.values()[ProtocolUtils.readVarInt(buf)]; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderFactory.java new file mode 100644 index 00000000..cc415b69 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.builder; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedChatBuilder; +import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatBuilder; +import com.velocitypowered.proxy.protocol.packet.chat.session.SessionChatBuilder; +import java.util.function.Function; + +public class ChatBuilderFactory { + private final ProtocolVersion version; + private final Function builderFunction; + + public ChatBuilderFactory(ProtocolVersion version) { + this.version = version; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { + this.builderFunction = SessionChatBuilder::new; + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + this.builderFunction = KeyedChatBuilder::new; + } else { + this.builderFunction = LegacyChatBuilder::new; + } + } + + public ChatBuilderV2 builder() { + return this.builderFunction.apply(this.version); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java new file mode 100644 index 00000000..b96fe387 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.builder; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ChatType; +import java.time.Instant; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public abstract class ChatBuilderV2 { + protected final ProtocolVersion version; + protected @MonotonicNonNull Component component; + protected @MonotonicNonNull String message; + protected @Nullable Player sender; + protected @Nullable Identity senderIdentity; + protected Instant timestamp; + protected ChatType type = ChatType.CHAT; + + protected ChatBuilderV2(ProtocolVersion version) { + this.version = version; + this.timestamp = Instant.now(); + } + + public ChatBuilderV2 component(Component component) { + this.component = component; + return this; + } + + public ChatBuilderV2 message(String message) { + this.message = message; + return this; + } + + public ChatBuilderV2 setType(ChatType chatType) { + this.type = chatType; + return this; + } + + public ChatBuilderV2 setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + public ChatBuilderV2 forIdentity(Identity identity) { + this.senderIdentity = identity; + return this; + } + + public ChatBuilderV2 asPlayer(@Nullable Player player) { + this.sender = player; + return this; + } + + public ChatBuilderV2 asServer() { + this.senderIdentity = null; + return this; + } + + public abstract MinecraftPacket toClient(); + + public abstract MinecraftPacket toServer(); +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatBuilder.java new file mode 100644 index 00000000..ab377ee9 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.keyed; + +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ChatType; +import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; +import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; +import net.kyori.adventure.text.Component; + +public class KeyedChatBuilder extends ChatBuilderV2 { + public KeyedChatBuilder(ProtocolVersion version) { + super(version); + } + + @Override + public MinecraftPacket toClient() { + // This is temporary + Component msg = component == null ? Component.text(message) : component; + return new SystemChat(msg, type == ChatType.CHAT ? ChatType.SYSTEM : type); + } + + @Override + public MinecraftPacket toServer() { + if (message.startsWith("/")) { + return new KeyedPlayerCommand(message.substring(1), ImmutableList.of(), timestamp); + } else { + // This will produce an error on the server, but needs to be here. + KeyedPlayerChat v1Chat = new KeyedPlayerChat(message); + v1Chat.setExpiry(this.timestamp); + return v1Chat; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java new file mode 100644 index 00000000..9f895295 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.keyed; + +import com.velocitypowered.api.event.EventManager; +import com.velocitypowered.api.event.player.PlayerChatEvent; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import net.kyori.adventure.text.Component; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class KeyedChatHandler implements com.velocitypowered.proxy.protocol.packet.chat.ChatHandler { + private static final Logger logger = LogManager.getLogger(KeyedChatHandler.class); + + private final VelocityServer server; + private final ConnectedPlayer player; + + public KeyedChatHandler(VelocityServer server, ConnectedPlayer player) { + this.server = server; + this.player = player; + } + + @Override + public Class packetClass() { + return KeyedPlayerChat.class; + } + + public static void invalidCancel(Logger logger, ConnectedPlayer player) { + logger.fatal("A plugin tried to cancel a signed chat message." + + " This is no longer possible in 1.19.1 and newer. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " + + "Contact your network administrator.")); + } + + public static void invalidChange(Logger logger, ConnectedPlayer player) { + logger.fatal("A plugin tried to change a signed chat message. " + + "This is no longer possible in 1.19.1 and newer. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " + + "Contact your network administrator.")); + } + + @Override + public void handlePlayerChatInternal(KeyedPlayerChat packet) { + ChatQueue chatQueue = this.player.getChatQueue(); + EventManager eventManager = this.server.getEventManager(); + PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage()); + CompletableFuture future = eventManager.fire(toSend); + + CompletableFuture chatFuture; + IdentifiedKey playerKey = this.player.getIdentifiedKey(); + + if (playerKey != null && !packet.isUnsigned()) { + // 1.19->1.19.2 signed version + chatFuture = future.thenApply(handleOldSignedChat(packet)); + } else { + // 1.19->1.19.2 unsigned version + chatFuture = future.thenApply(pme -> { + PlayerChatEvent.ChatResult chatResult = pme.getResult(); + if (!chatResult.isAllowed()) { + return null; + } + + return player.getChatBuilderFactory().builder() + .message(chatResult.getMessage().orElse(packet.getMessage())).setTimestamp(packet.getExpiry()).toServer(); + }); + } + chatQueue.queuePacket( + chatFuture.exceptionally((ex) -> { + logger.error("Exception while handling player chat for {}", player, ex); + return null; + }), + packet.getExpiry() + ); + } + + private Function handleOldSignedChat(KeyedPlayerChat packet) { + IdentifiedKey playerKey = this.player.getIdentifiedKey(); + assert playerKey != null; + return pme -> { + PlayerChatEvent.ChatResult chatResult = pme.getResult(); + if (!chatResult.isAllowed() && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { + invalidCancel(logger, player); + return null; + } + + if (chatResult.getMessage().map(str -> !str.equals(packet.getMessage())).orElse(false)) { + if (playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { + // Bad, very bad. + invalidChange(logger, player); + } else { + logger.warn("A plugin changed a signed chat message. The server may not accept it."); + return player.getChatBuilderFactory().builder() + .message(chatResult.getMessage().get() /* always present at this point */) + .setTimestamp(packet.getExpiry()) + .toServer(); + } + } + return packet; + }; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java new file mode 100644 index 00000000..2427c973 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.keyed; + +import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; + +public class KeyedCommandHandler implements CommandHandler { + private final ConnectedPlayer player; + private final VelocityServer server; + + public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) { + this.player = player; + this.server = server; + } + + @Override + public Class packetClass() { + return KeyedPlayerCommand.class; + } + + @Override + public void handlePlayerCommandInternal(KeyedPlayerCommand packet) { + queueCommandResult(this.server, this.player, event -> { + CommandExecuteEvent.CommandResult result = event.getResult(); + IdentifiedKey playerKey = player.getIdentifiedKey(); + if (result == CommandExecuteEvent.CommandResult.denied()) { + if (playerKey != null) { + if (!packet.isUnsigned() && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { + logger.fatal("A plugin tried to deny a command with signable component(s). " + "This is not supported. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text( + "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); + } + } + return CompletableFuture.completedFuture(null); + } + + String commandToRun = result.getCommand().orElse(packet.getCommand()); + if (result.isForwardToServer()) { + ChatBuilderV2 write = this.player.getChatBuilderFactory() + .builder() + .setTimestamp(packet.getTimestamp()) + .asPlayer(this.player); + + if (!packet.isUnsigned() && commandToRun.equals(packet.getCommand())) { + return CompletableFuture.completedFuture(packet); + } else { + if (!packet.isUnsigned() && playerKey != null + && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { + logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text( + "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); + return CompletableFuture.completedFuture(null); + } + write.message("/" + commandToRun); + } + return CompletableFuture.completedFuture(write.toServer()); + } + return runCommand(this.server, this.player, commandToRun, hasRun -> { + if (!hasRun) { + if (commandToRun.equals(packet.getCommand())) { + return packet; + } + + if (!packet.isUnsigned() && playerKey != null + && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { + logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text( + "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); + return null; + } + + return this.player.getChatBuilderFactory() + .builder() + .setTimestamp(packet.getTimestamp()) + .asPlayer(this.player) + .message("/" + commandToRun) + .toServer(); + } + return null; + }); + }, packet.getCommand(), packet.getTimestamp()); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedPlayerChat.java similarity index 71% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChat.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedPlayerChat.java index 9c675b51..f3a88671 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChat.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedPlayerChat.java @@ -15,24 +15,21 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet.chat; +package com.velocitypowered.proxy.protocol.packet.chat.keyed; import com.google.common.primitives.Longs; import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.crypto.SignaturePair; -import com.velocitypowered.proxy.crypto.SignedChatMessage; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import java.time.Instant; -import java.util.UUID; import org.checkerframework.checker.nullness.qual.Nullable; -public class PlayerChat implements MinecraftPacket { +public class KeyedPlayerChat implements MinecraftPacket { private String message; private boolean signedPreview; @@ -46,29 +43,18 @@ public class PlayerChat implements MinecraftPacket { public static final int MAXIMUM_PREVIOUS_MESSAGE_COUNT = 5; public static final QuietDecoderException INVALID_PREVIOUS_MESSAGES = - new QuietDecoderException("Invalid previous messages"); + new QuietDecoderException("Invalid previous messages"); - public PlayerChat() { + public KeyedPlayerChat() { } - public PlayerChat(String message) { + public KeyedPlayerChat(String message) { this.message = message; this.unsigned = true; } - /** - * Create new {@link PlayerChat} based on a previously {@link SignedChatMessage}. - * - * @param message The {@link SignedChatMessage} to turn into {@link PlayerChat}. - */ - public PlayerChat(SignedChatMessage message) { - this.message = message.getMessage(); - this.expiry = message.getExpiryTemporal(); - this.salt = message.getSalt(); - this.signature = message.getSignature(); - this.signedPreview = message.isPreviewSigned(); - this.lastMessage = message.getPreviousSignature(); - this.previousMessages = message.getPreviousSignatures(); + public void setExpiry(@Nullable Instant expiry) { + this.expiry = expiry; } public Instant getExpiry() { @@ -135,11 +121,11 @@ public class PlayerChat implements MinecraftPacket { buf.writeLong(unsigned ? Instant.now().toEpochMilli() : expiry.toEpochMilli()); buf.writeLong(unsigned ? 0L : Longs.fromByteArray(salt)); + ProtocolUtils.writeByteArray(buf, unsigned ? EncryptionUtils.EMPTY : signature); buf.writeBoolean(signedPreview); - if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { ProtocolUtils.writeVarInt(buf, previousMessages.length); for (SignaturePair previousMessage : previousMessages) { @@ -157,28 +143,6 @@ public class PlayerChat implements MinecraftPacket { } } - /** - * Validates a signature and creates a {@link SignedChatMessage} from the given signature. - * - * @param signer the signer's information - * @param sender the sender of the message - * @param mustSign instructs the function to throw if the signature is invalid. - * @return The {@link SignedChatMessage} or null if the signature couldn't be verified. - * @throws com.velocitypowered.proxy.util.except.QuietDecoderException when mustSign is {@code true} and the signature - * is invalid. - */ - public SignedChatMessage signedContainer(IdentifiedKey signer, UUID sender, boolean mustSign) { - if (unsigned) { - if (mustSign) { - throw EncryptionUtils.INVALID_SIGNATURE; - } - return null; - } - - return new SignedChatMessage(message, signer.getSignedPublicKey(), sender, expiry, signature, - salt, signedPreview, previousMessages, lastMessage); - } - @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedPlayerCommand.java similarity index 63% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerCommand.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedPlayerCommand.java index ab035462..661026b4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedPlayerCommand.java @@ -15,20 +15,16 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet.chat; +package com.velocitypowered.proxy.protocol.packet.chat.keyed; -import static com.velocitypowered.proxy.protocol.packet.chat.PlayerChat.INVALID_PREVIOUS_MESSAGES; -import static com.velocitypowered.proxy.protocol.packet.chat.PlayerChat.MAXIMUM_PREVIOUS_MESSAGE_COUNT; +import static com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat.INVALID_PREVIOUS_MESSAGES; +import static com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat.MAXIMUM_PREVIOUS_MESSAGE_COUNT; import com.google.common.collect.ImmutableMap; -import com.google.common.primitives.Longs; import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.crypto.SignaturePair; -import com.velocitypowered.proxy.crypto.SignedChatCommand; -import com.velocitypowered.proxy.crypto.SignedChatMessage; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.util.except.QuietDecoderException; @@ -37,10 +33,9 @@ import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.UUID; import org.checkerframework.checker.nullness.qual.Nullable; -public class PlayerCommand implements MinecraftPacket { +public class KeyedPlayerCommand implements MinecraftPacket { private static final int MAX_NUM_ARGUMENTS = 8; private static final int MAX_LENGTH_ARGUMENTS = 16; @@ -51,15 +46,11 @@ public class PlayerCommand implements MinecraftPacket { private String command; private Instant timestamp; private long salt; - private boolean signedPreview; // Good god. Please no. + private boolean signedPreview; // purely for pass through for 1.19 -> 1.19.2 - this will never be implemented private SignaturePair[] previousMessages = new SignaturePair[0]; private @Nullable SignaturePair lastMessage; private Map arguments = ImmutableMap.of(); - public boolean isSignedPreview() { - return signedPreview; - } - public Instant getTimestamp() { return timestamp; } @@ -72,17 +63,17 @@ public class PlayerCommand implements MinecraftPacket { return command; } - public PlayerCommand() { + public KeyedPlayerCommand() { } /** - * Creates an {@link PlayerCommand} packet based on a command and list of arguments. + * Creates an {@link KeyedPlayerCommand} packet based on a command and list of arguments. * - * @param command the command to run + * @param command the command to run * @param arguments the arguments of the command * @param timestamp the timestamp of the command execution */ - public PlayerCommand(String command, List arguments, Instant timestamp) { + public KeyedPlayerCommand(String command, List arguments, Instant timestamp) { this.unsigned = true; ImmutableMap.Builder builder = ImmutableMap.builder(); arguments.forEach(entry -> builder.put(entry, EncryptionUtils.EMPTY)); @@ -93,21 +84,6 @@ public class PlayerCommand implements MinecraftPacket { this.salt = 0L; } - /** - * Create new {@link PlayerCommand} based on a previously {@link SignedChatCommand}. - * - * @param signedCommand The {@link SignedChatCommand} to turn into {@link PlayerCommand}. - */ - public PlayerCommand(SignedChatCommand signedCommand) { - this.command = signedCommand.getBaseCommand(); - this.arguments = ImmutableMap.copyOf(signedCommand.getSignatures()); - this.timestamp = signedCommand.getExpiryTemporal(); - this.salt = Longs.fromByteArray(signedCommand.getSalt()); - this.signedPreview = signedCommand.isPreviewSigned(); - this.lastMessage = signedCommand.getLastSignature(); - this.previousMessages = signedCommand.getPreviousSignatures(); - } - @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { command = ProtocolUtils.readString(buf, 256); @@ -127,7 +103,7 @@ public class PlayerCommand implements MinecraftPacket { } arguments = entries.build(); - signedPreview = buf.readBoolean(); + this.signedPreview = buf.readBoolean(); if (unsigned && signedPreview) { throw EncryptionUtils.PREVIEW_SIGNATURE_MISSING; } @@ -193,43 +169,17 @@ public class PlayerCommand implements MinecraftPacket { } - /** - * Validates a signature and creates a {@link SignedChatCommand} from the given signature. - * - * @param signer the signer's information - * @param sender the sender of the message - * @param mustSign instructs the function to throw if the signature is invalid. - * @return The {@link SignedChatCommand} or null if the signature couldn't be verified. - * @throws com.velocitypowered.proxy.util.except.QuietDecoderException when mustSign is {@code true} and the signature - * is invalid. - */ - public SignedChatCommand signedContainer( - @Nullable IdentifiedKey signer, UUID sender, boolean mustSign) { - // There's a certain mod that is very broken that still signs messages but - // doesn't provide the player key. This is broken and wrong, but we need to - // work around that. - if (unsigned || signer == null) { - if (mustSign) { - throw EncryptionUtils.INVALID_SIGNATURE; - } - return null; - } - - return new SignedChatCommand(command, signer.getSignedPublicKey(), sender, timestamp, - arguments, Longs.toByteArray(salt), signedPreview, previousMessages, lastMessage); - } - @Override public String toString() { return "PlayerCommand{" - + "unsigned=" + unsigned - + ", command='" + command + '\'' - + ", timestamp=" + timestamp - + ", salt=" + salt - + ", signedPreview=" + signedPreview - + ", previousMessages=" + Arrays.toString(previousMessages) - + ", arguments=" + arguments - + '}'; + + "unsigned=" + unsigned + + ", command='" + command + '\'' + + ", timestamp=" + timestamp + + ", salt=" + salt + + ", signedPreview=" + signedPreview + + ", previousMessages=" + Arrays.toString(previousMessages) + + ", arguments=" + arguments + + '}'; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LegacyChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChat.java similarity index 95% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LegacyChat.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChat.java index d16d4c5e..0314994c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LegacyChat.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChat.java @@ -15,16 +15,14 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet.chat; +package com.velocitypowered.proxy.protocol.packet.chat.legacy; -import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.UUID; -import net.kyori.adventure.identity.Identity; import org.checkerframework.checker.nullness.qual.Nullable; public class LegacyChat implements MinecraftPacket { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChatPreview.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatBuilder.java similarity index 50% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChatPreview.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatBuilder.java index bf9079a1..4bdaccd3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChatPreview.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatBuilder.java @@ -15,41 +15,35 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet.chat; +package com.velocitypowered.proxy.protocol.packet.chat.legacy; import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; +import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; +import java.util.UUID; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; -public class PlayerChatPreview implements MinecraftPacket { - - private int id; - private String query; - - public int getId() { - return id; - } - - public String getQuery() { - return query; +public class LegacyChatBuilder extends ChatBuilderV2 { + public LegacyChatBuilder(ProtocolVersion version) { + super(version); } @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - id = buf.readInt(); - query = ProtocolUtils.readString(buf, 256); + public MinecraftPacket toClient() { + // This is temporary + UUID identity = sender == null ? (senderIdentity == null ? Identity.nil().uuid() + : senderIdentity.uuid()) : sender.getUniqueId(); + Component msg = component == null ? Component.text(message) : component; + + return new LegacyChat(ProtocolUtils.getJsonChatSerializer(version).serialize(msg), type.getId(), identity); } @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - buf.writeInt(id); - ProtocolUtils.writeString(buf, query); - } - - @Override - public boolean handle(MinecraftSessionHandler handler) { - return handler.handle(this); + public MinecraftPacket toServer() { + LegacyChat chat = new LegacyChat(); + chat.setMessage(message); + return chat; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatHandler.java new file mode 100644 index 00000000..ef751bf4 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.legacy; + +import com.velocitypowered.api.event.player.PlayerChatEvent; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler; + +public class LegacyChatHandler implements ChatHandler { + private final VelocityServer server; + private final ConnectedPlayer player; + + public LegacyChatHandler(VelocityServer server, ConnectedPlayer player) { + this.server = server; + this.player = player; + } + + @Override + public Class packetClass() { + return LegacyChat.class; + } + + @Override + public void handlePlayerChatInternal(LegacyChat packet) { + MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected(); + if (serverConnection == null) { + return; + } + this.server.getEventManager().fire(new PlayerChatEvent(this.player, packet.getMessage())) + .whenComplete((chatEvent, throwable) -> { + if (chatEvent.getResult().isAllowed()) { + return; + } + + serverConnection.write(this.player.getChatBuilderFactory().builder() + .message(chatEvent.getResult().getMessage().orElse(packet.getMessage())).toServer()); + }); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java new file mode 100644 index 00000000..c15d11c5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.legacy; + +import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; + +public class LegacyCommandHandler implements CommandHandler { + private final ConnectedPlayer player; + private final VelocityServer server; + + public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) { + this.player = player; + this.server = server; + } + + @Override + public Class packetClass() { + return LegacyChat.class; + } + + @Override + public void handlePlayerCommandInternal(LegacyChat packet) { + String command = packet.getMessage().substring(1); + queueCommandResult(this.server, this.player, event -> { + CommandExecuteEvent.CommandResult result = event.getResult(); + if (!result.isAllowed()) { + return CompletableFuture.completedFuture(null); + } + String commandToRun = result.getCommand().orElse(command); + if (result.isForwardToServer()) { + return CompletableFuture.completedFuture(this.player.getChatBuilderFactory().builder() + .message("/" + commandToRun) + .toServer()); + } + return runCommand(this.server, this.player, commandToRun, hasRun -> { + if (!hasRun) { + return this.player.getChatBuilderFactory().builder() + .message(packet.getMessage()) + .asPlayer(this.player) + .toServer(); + } + return null; + }); + }, command, Instant.now()); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java new file mode 100644 index 00000000..8d44317c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.session; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ChatType; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; +import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; +import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; +import net.kyori.adventure.text.Component; + +public class SessionChatBuilder extends ChatBuilderV2 { + public SessionChatBuilder(ProtocolVersion version) { + super(version); + } + + @Override + public MinecraftPacket toClient() { + // This is temporary + Component msg = component == null ? Component.text(message) : component; + return new SystemChat(msg, type == ChatType.CHAT ? ChatType.SYSTEM : type); + } + + @Override + public MinecraftPacket toServer() { + if (message.startsWith("/")) { + SessionPlayerCommand command = new SessionPlayerCommand(); + command.command = message.substring(1); + command.salt = 0L; + command.timeStamp = timestamp; + command.argumentSignatures = new SessionPlayerCommand.ArgumentSignatures(); + command.lastSeenMessages = new LastSeenMessages(); + return command; + } else { + SessionPlayerChat chat = new SessionPlayerChat(); + chat.message = message; + chat.signed = false; + chat.signature = new byte[0]; + chat.timestamp = timestamp; + chat.salt = 0L; + return chat; + } + } +} \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java new file mode 100644 index 00000000..5085d78f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.session; + +import static com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedChatHandler.invalidCancel; +import static com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedChatHandler.invalidChange; + +import com.velocitypowered.api.event.EventManager; +import com.velocitypowered.api.event.player.PlayerChatEvent; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler; +import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SessionChatHandler implements ChatHandler { + private static final Logger logger = LogManager.getLogger(SessionChatHandler.class); + + private final ConnectedPlayer player; + private final VelocityServer server; + + public SessionChatHandler(ConnectedPlayer player, VelocityServer server) { + this.player = player; + this.server = server; + } + + @Override + public Class packetClass() { + return SessionPlayerChat.class; + } + + @Override + public void handlePlayerChatInternal(SessionPlayerChat packet) { + ChatQueue chatQueue = this.player.getChatQueue(); + EventManager eventManager = this.server.getEventManager(); + PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage()); + chatQueue.queuePacket( + eventManager.fire(toSend) + .thenApply(pme -> { + PlayerChatEvent.ChatResult chatResult = pme.getResult(); + if (!chatResult.isAllowed()) { + if (packet.isSigned()) { + invalidCancel(logger, player); + } + return null; + } + + if (chatResult.getMessage().map(str -> !str.equals(packet.getMessage())).orElse(false)) { + if (packet.isSigned()) { + invalidChange(logger, player); + return null; + } + return this.player.getChatBuilderFactory().builder().message(packet.message) + .setTimestamp(packet.timestamp) + .toServer(); + } + return packet; + }) + .exceptionally((ex) -> { + logger.error("Exception while handling player chat for {}", player, ex); + return null; + }), + packet.getTimestamp() + ); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java new file mode 100644 index 00000000..4ed20768 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.session; + +import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; + +public class SessionCommandHandler implements CommandHandler { + private final ConnectedPlayer player; + private final VelocityServer server; + + public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) { + this.player = player; + this.server = server; + } + + @Override + public Class packetClass() { + return SessionPlayerCommand.class; + } + + @Override + public void handlePlayerCommandInternal(SessionPlayerCommand packet) { + queueCommandResult(this.server, this.player, event -> { + CommandExecuteEvent.CommandResult result = event.getResult(); + if (!result.isAllowed()) { + if (!packet.argumentSignatures.isEmpty()) { + logger.fatal("A plugin tried to deny a command with signable component(s). " + "This is not supported. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text( + "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); + } + return CompletableFuture.completedFuture(null); + } + + String commandToRun = result.getCommand().orElse(packet.command); + if (result.isForwardToServer()) { + if (!packet.argumentSignatures.isEmpty() && commandToRun.equals(packet.command)) { + return CompletableFuture.completedFuture(packet); + } else { + if (!packet.argumentSignatures.isEmpty()) { + logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text( + "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); + return CompletableFuture.completedFuture(null); + } + + return CompletableFuture.completedFuture(this.player.getChatBuilderFactory() + .builder() + .setTimestamp(packet.timeStamp) + .asPlayer(this.player) + .message("/" + commandToRun) + .toServer()); + } + } + + return runCommand(this.server, this.player, commandToRun, hasRun -> { + if (!hasRun) { + if (!packet.argumentSignatures.isEmpty() && commandToRun.equals(packet.command)) { + return packet; + } else { + if (!packet.argumentSignatures.isEmpty()) { + logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " + + "Disconnecting player " + player.getUsername()); + player.disconnect(Component.text( + "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); + return null; + } + + return this.player.getChatBuilderFactory() + .builder() + .setTimestamp(packet.timeStamp) + .asPlayer(this.player) + .message("/" + commandToRun) + .toServer(); + } + } + return null; + }); + }, packet.command, packet.timeStamp); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChat.java new file mode 100644 index 00000000..9f6130c1 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChat.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.session; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; +import io.netty.buffer.ByteBuf; +import java.time.Instant; + +public class SessionPlayerChat implements MinecraftPacket { + protected String message; + protected Instant timestamp; + protected long salt; + protected boolean signed; + protected byte[] signature; + protected LastSeenMessages lastSeenMessages; + + public SessionPlayerChat() { + } + + public String getMessage() { + return message; + } + + public Instant getTimestamp() { + return timestamp; + } + + public long getSalt() { + return salt; + } + + public boolean isSigned() { + return signed; + } + + public byte[] getSignature() { + return signature; + } + + public LastSeenMessages getLastSeenMessages() { + return lastSeenMessages; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + this.message = ProtocolUtils.readString(buf, 256); + this.timestamp = Instant.ofEpochMilli(buf.readLong()); + this.salt = buf.readLong(); + this.signed = buf.readBoolean(); + if (this.signed) { + this.signature = readMessageSignature(buf); + } else { + this.signature = new byte[0]; + } + this.lastSeenMessages = new LastSeenMessages(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, this.message); + buf.writeLong(this.timestamp.toEpochMilli()); + buf.writeLong(this.salt); + buf.writeBoolean(this.signed); + if (this.signed) { + buf.writeBytes(this.signature); + } + this.lastSeenMessages.encode(buf); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + protected static byte[] readMessageSignature(ByteBuf buf) { + byte[] signature = new byte[256]; + buf.readBytes(signature); + return signature; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java new file mode 100644 index 00000000..81c6d8b4 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat.session; + +import com.google.common.collect.Lists; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; +import com.velocitypowered.proxy.util.except.QuietDecoderException; +import io.netty.buffer.ByteBuf; +import java.time.Instant; +import java.util.List; + +public class SessionPlayerCommand implements MinecraftPacket { + protected String command; + protected Instant timeStamp; + protected long salt; + protected ArgumentSignatures argumentSignatures; + protected LastSeenMessages lastSeenMessages; + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + this.command = ProtocolUtils.readString(buf, 256); + this.timeStamp = Instant.ofEpochMilli(buf.readLong()); + this.salt = buf.readLong(); + this.argumentSignatures = new ArgumentSignatures(buf); + this.lastSeenMessages = new LastSeenMessages(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, this.command); + buf.writeLong(this.timeStamp.toEpochMilli()); + buf.writeLong(this.salt); + this.argumentSignatures.encode(buf); + this.lastSeenMessages.encode(buf); + } + + public String getCommand() { + return command; + } + + public Instant getTimeStamp() { + return timeStamp; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + public static class ArgumentSignatures { + private final List entries; + + public ArgumentSignatures() { + this.entries = List.of(); + } + + public ArgumentSignatures(ByteBuf buf) { + int size = ProtocolUtils.readVarInt(buf); + if (size > 8) { + throw new QuietDecoderException(String.format("Too many argument signatures, %d is above limit %d", size, 8)); + } + + this.entries = Lists.newArrayListWithCapacity(size); + for (int i = 0; i < size; i++) { + this.entries.add(new ArgumentSignature(buf)); + } + } + + public boolean isEmpty() { + return this.entries.isEmpty(); + } + + public void encode(ByteBuf buf) { + ProtocolUtils.writeVarInt(buf, entries.size()); + for (ArgumentSignature entry : entries) { + entry.encode(buf); + } + } + } + + public static class ArgumentSignature { + private final String name; + private final byte[] signature; + + public ArgumentSignature(ByteBuf buf) { + name = ProtocolUtils.readString(buf, 16); + signature = SessionPlayerChat.readMessageSignature(buf); + } + + public void encode(ByteBuf buf) { + ProtocolUtils.writeString(buf, name); + buf.writeBytes(signature); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java new file mode 100644 index 00000000..0d8d5c51 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.tablist; + +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; + +public interface InternalTabList extends TabList { + default void processLegacy(LegacyPlayerListItem packet) { + } + + default void processUpdate(UpsertPlayerInfo infoPacket) { + } + + default void processRemove(RemovePlayerInfo infoPacket) { + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java new file mode 100644 index 00000000..5744e6c3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.tablist; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.player.ChatSession; +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.proxy.player.TabListEntry; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class KeyedVelocityTabList implements InternalTabList { + + protected final ConnectedPlayer player; + protected final MinecraftConnection connection; + protected final ProxyServer proxyServer; + protected final Map entries = new ConcurrentHashMap<>(); + + /** + * Creates a new VelocityTabList. + */ + public KeyedVelocityTabList(final ConnectedPlayer player, final ProxyServer proxyServer) { + this.player = player; + this.proxyServer = proxyServer; + this.connection = player.getConnection(); + } + + @Deprecated + @Override + public void setHeaderAndFooter(Component header, Component footer) { + Preconditions.checkNotNull(header, "header"); + Preconditions.checkNotNull(footer, "footer"); + this.player.sendPlayerListHeaderAndFooter(header, footer); + } + + @Override + public void clearHeaderAndFooter() { + connection.write(HeaderAndFooter.reset()); + } + + @Override + public void addEntry(TabListEntry entry) { + Preconditions.checkNotNull(entry, "entry"); + Preconditions.checkArgument(entry.getTabList().equals(this), + "The provided entry was not created by this tab list"); + Preconditions.checkArgument(!entries.containsKey(entry.getProfile().getId()), + "this TabList already contains an entry with the same uuid"); + Preconditions.checkArgument(entry instanceof KeyedVelocityTabListEntry, + "Not a Velocity tab list entry"); + + LegacyPlayerListItem.Item packetItem = LegacyPlayerListItem.Item.from(entry); + connection.write( + new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem))); + entries.put(entry.getProfile().getId(), (KeyedVelocityTabListEntry) entry); + } + + @Override + public Optional removeEntry(UUID uuid) { + Preconditions.checkNotNull(uuid, "uuid"); + + TabListEntry entry = entries.remove(uuid); + if (entry != null) { + LegacyPlayerListItem.Item packetItem = LegacyPlayerListItem.Item.from(entry); + connection.write( + new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, Collections.singletonList(packetItem))); + } + + return Optional.ofNullable(entry); + } + + @Override + public boolean containsEntry(UUID uuid) { + Preconditions.checkNotNull(uuid, "uuid"); + return entries.containsKey(uuid); + } + + /** + * Clears all entries from the tab list. Note that the entries are written with {@link + * MinecraftConnection#delayedWrite(Object)}, so make sure to do an explicit {@link + * MinecraftConnection#flush()}. + */ + @Override + public void clearAll() { + Collection listEntries = entries.values(); + if (listEntries.isEmpty()) { + return; + } + List items = new ArrayList<>(listEntries.size()); + for (TabListEntry value : listEntries) { + items.add(LegacyPlayerListItem.Item.from(value)); + } + entries.clear(); + connection.delayedWrite(new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, items)); + } + + @Override + public Collection getEntries() { + return Collections.unmodifiableCollection(this.entries.values()); + } + + @Override + public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode) { + return buildEntry(profile, displayName, latency, gameMode, (ChatSession) null); + } + + @Override + public TabListEntry buildEntry(GameProfile profile, + net.kyori.adventure.text.@Nullable Component displayName, + int latency, int gameMode, @Nullable IdentifiedKey key) { + return new KeyedVelocityTabListEntry(this, profile, displayName, latency, gameMode, key); + } + + @Override + public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode, + @Nullable ChatSession chatSession) { + return new KeyedVelocityTabListEntry(this, profile, displayName, latency, gameMode, + chatSession == null ? null : chatSession.getIdentifiedKey()); + } + + @Override + public void processLegacy(LegacyPlayerListItem packet) { + // Packets are already forwarded on, so no need to do that here + for (LegacyPlayerListItem.Item item : packet.getItems()) { + UUID uuid = item.getUuid(); + assert uuid != null : "1.7 tab list entry given to modern tab list handler!"; + + if (packet.getAction() != LegacyPlayerListItem.ADD_PLAYER && !entries.containsKey(uuid)) { + // Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here + continue; + } + + switch (packet.getAction()) { + case LegacyPlayerListItem.ADD_PLAYER: { + // ensure that name and properties are available + String name = item.getName(); + List properties = item.getProperties(); + if (name == null || properties == null) { + throw new IllegalStateException("Got null game profile for ADD_PLAYER"); + } + /* why are we verifying the key here - multi-proxy setups break this + // Verify key + IdentifiedKey providedKey = item.getPlayerKey(); + Optional connected = proxyServer.getPlayer(uuid); + if (connected.isPresent()) { + IdentifiedKey expectedKey = connected.get().getIdentifiedKey(); + if (providedKey != null) { + if (!Objects.equals(expectedKey, providedKey)) { + throw new IllegalStateException("Server provided incorrect player key in playerlist for " + + name + " UUID: " + uuid); + } + } else { + // Substitute the key + // It shouldn't be propagated to remove the signature. + providedKey = expectedKey; + } + } + */ + + entries.putIfAbsent(item.getUuid(), (KeyedVelocityTabListEntry) TabListEntry.builder() + .tabList(this) + .profile(new GameProfile(uuid, name, properties)) + .displayName(item.getDisplayName()) + .latency(item.getLatency()) + .chatSession(new RemoteChatSession(null, item.getPlayerKey())) + .gameMode(item.getGameMode()) + .build()); + break; + } + case LegacyPlayerListItem.REMOVE_PLAYER: + entries.remove(uuid); + break; + case LegacyPlayerListItem.UPDATE_DISPLAY_NAME: { + KeyedVelocityTabListEntry entry = entries.get(uuid); + if (entry != null) { + entry.setDisplayNameInternal(item.getDisplayName()); + } + break; + } + case LegacyPlayerListItem.UPDATE_LATENCY: { + KeyedVelocityTabListEntry entry = entries.get(uuid); + if (entry != null) { + entry.setLatencyInternal(item.getLatency()); + } + break; + } + case LegacyPlayerListItem.UPDATE_GAMEMODE: { + KeyedVelocityTabListEntry entry = entries.get(uuid); + if (entry != null) { + entry.setGameModeInternal(item.getGameMode()); + } + break; + } + default: + // Nothing we can do here + break; + } + } + } + + void updateEntry(int action, TabListEntry entry) { + if (entries.containsKey(entry.getProfile().getId())) { + LegacyPlayerListItem.Item packetItem = LegacyPlayerListItem.Item.from(entry); + + IdentifiedKey selectedKey = packetItem.getPlayerKey(); + Optional existing = proxyServer.getPlayer(entry.getProfile().getId()); + if (existing.isPresent()) { + selectedKey = existing.get().getIdentifiedKey(); + } + + if (selectedKey != null + && selectedKey.getKeyRevision().getApplicableTo().contains(connection.getProtocolVersion()) + && Objects.equals(selectedKey.getSignatureHolder(), entry.getProfile().getId())) { + packetItem.setPlayerKey(selectedKey); + } else { + packetItem.setPlayerKey(null); + } + + connection.write(new LegacyPlayerListItem(action, Collections.singletonList(packetItem))); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabListEntry.java new file mode 100644 index 00000000..04999165 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabListEntry.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.tablist; + +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.player.ChatSession; +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.proxy.player.TabListEntry; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; +import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class KeyedVelocityTabListEntry implements TabListEntry { + + private final KeyedVelocityTabList tabList; + private final GameProfile profile; + private net.kyori.adventure.text.Component displayName; + private int latency; + private int gameMode; + private @Nullable IdentifiedKey playerKey; + + KeyedVelocityTabListEntry(KeyedVelocityTabList tabList, GameProfile profile, + net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode, + @Nullable IdentifiedKey playerKey) { + this.tabList = tabList; + this.profile = profile; + this.displayName = displayName; + this.latency = latency; + this.gameMode = gameMode; + this.playerKey = playerKey; + } + + @Override + public TabList getTabList() { + return tabList; + } + + @Override + public GameProfile getProfile() { + return profile; + } + + @Override + public Optional getDisplayNameComponent() { + return Optional.ofNullable(displayName); + } + + @Override + public TabListEntry setDisplayName(net.kyori.adventure.text.@Nullable Component displayName) { + this.displayName = displayName; + tabList.updateEntry(LegacyPlayerListItem.UPDATE_DISPLAY_NAME, this); + return this; + } + + void setDisplayNameInternal(net.kyori.adventure.text.@Nullable Component displayName) { + this.displayName = displayName; + } + + @Override + public int getLatency() { + return latency; + } + + @Override + public TabListEntry setLatency(int latency) { + this.latency = latency; + tabList.updateEntry(LegacyPlayerListItem.UPDATE_LATENCY, this); + return this; + } + + void setLatencyInternal(int latency) { + this.latency = latency; + } + + @Override + public int getGameMode() { + return gameMode; + } + + @Override + public TabListEntry setGameMode(int gameMode) { + this.gameMode = gameMode; + tabList.updateEntry(LegacyPlayerListItem.UPDATE_GAMEMODE, this); + return this; + } + + void setGameModeInternal(int gameMode) { + this.gameMode = gameMode; + } + + @Override + public @Nullable ChatSession getChatSession() { + return new RemoteChatSession(null, this.playerKey); + } + + @Override + public IdentifiedKey getIdentifiedKey() { + return playerKey; + } + + void setPlayerKeyInternal(IdentifiedKey playerKey) { + this.playerKey = playerKey; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index eeb059b2..47f48707 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -18,46 +18,45 @@ package com.velocitypowered.proxy.tablist; import com.google.common.base.Preconditions; -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ProxyServer; +import com.google.common.collect.Maps; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; -import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.proxy.player.ChatSession; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import net.kyori.adventure.text.Component; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; -public class VelocityTabList implements TabList { +public class VelocityTabList implements InternalTabList { + private static final Logger logger = LogManager.getLogger(VelocityConsole.class); + private final ConnectedPlayer player; + private final MinecraftConnection connection; + private final Map entries; - protected final ConnectedPlayer player; - protected final MinecraftConnection connection; - protected final ProxyServer proxyServer; - protected final Map entries = new ConcurrentHashMap<>(); - - /** - * Creates a new VelocityTabList. - */ - public VelocityTabList(final ConnectedPlayer player, final ProxyServer proxyServer) { + public VelocityTabList(ConnectedPlayer player) { this.player = player; - this.proxyServer = proxyServer; this.connection = player.getConnection(); + this.entries = Maps.newHashMap(); } - @Deprecated @Override public void setHeaderAndFooter(Component header, Component footer) { Preconditions.checkNotNull(header, "header"); @@ -71,177 +70,178 @@ public class VelocityTabList implements TabList { } @Override - public void addEntry(TabListEntry entry) { - Preconditions.checkNotNull(entry, "entry"); - Preconditions.checkArgument(entry.getTabList().equals(this), - "The provided entry was not created by this tab list"); - Preconditions.checkArgument(!entries.containsKey(entry.getProfile().getId()), - "this TabList already contains an entry with the same uuid"); - Preconditions.checkArgument(entry instanceof VelocityTabListEntry, - "Not a Velocity tab list entry"); + public void addEntry(TabListEntry entry1) { + VelocityTabListEntry entry; + if (entry1 instanceof VelocityTabListEntry) { + entry = (VelocityTabListEntry) entry1; + } else { + entry = new VelocityTabListEntry(this, entry1.getProfile(), entry1.getDisplayNameComponent().orElse(null), + entry1.getLatency(), entry1.getGameMode(), entry1.getChatSession(), entry1.isListed()); + } - PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry); - connection.write( - new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem))); - entries.put(entry.getProfile().getId(), (VelocityTabListEntry) entry); + EnumSet actions = EnumSet.noneOf(UpsertPlayerInfo.Action.class); + UpsertPlayerInfo.Entry playerInfoEntry = new UpsertPlayerInfo.Entry(entry.getProfile().getId()); + + Preconditions.checkNotNull(entry.getProfile(), "Profile cannot be null"); + Preconditions.checkNotNull(entry.getProfile().getId(), "Profile ID cannot be null"); + + TabListEntry previousEntry = this.entries.put(entry.getProfile().getId(), entry); + + if (previousEntry != null) { + // we should merge entries here + if (previousEntry.equals(entry)) { + return; // nothing else to do, this entry is perfect + } + if (!Objects.equals(previousEntry.getDisplayNameComponent().orElse(null), + entry.getDisplayNameComponent().orElse(null))) { + actions.add(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME); + playerInfoEntry.setDisplayName(entry.getDisplayNameComponent().get()); + } + if (!Objects.equals(previousEntry.getLatency(), entry.getLatency())) { + actions.add(UpsertPlayerInfo.Action.UPDATE_LATENCY); + playerInfoEntry.setLatency(entry.getLatency()); + } + if (!Objects.equals(previousEntry.getGameMode(), entry.getGameMode())) { + actions.add(UpsertPlayerInfo.Action.UPDATE_GAME_MODE); + playerInfoEntry.setGameMode(entry.getGameMode()); + } + if (!Objects.equals(previousEntry.isListed(), entry.isListed())) { + actions.add(UpsertPlayerInfo.Action.UPDATE_LISTED); + playerInfoEntry.setListed(entry.isListed()); + } + if (!Objects.equals(previousEntry.getChatSession(), entry.getChatSession())) { + ChatSession from = entry.getChatSession(); + if (from != null) { + actions.add(UpsertPlayerInfo.Action.INITIALIZE_CHAT); + playerInfoEntry.setChatSession(new RemoteChatSession(from.getSessionId(), from.getIdentifiedKey())); + } + } + } else { + actions.addAll(EnumSet.of(UpsertPlayerInfo.Action.ADD_PLAYER, + UpsertPlayerInfo.Action.UPDATE_LATENCY, + UpsertPlayerInfo.Action.UPDATE_LISTED)); + if (entry.getDisplayNameComponent().isPresent()) { + actions.add(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME); + playerInfoEntry.setDisplayName(entry.getDisplayNameComponent().get()); + } + if (entry.getChatSession() != null) { + actions.add(UpsertPlayerInfo.Action.INITIALIZE_CHAT); + ChatSession from = entry.getChatSession(); + playerInfoEntry.setChatSession(new RemoteChatSession(from.getSessionId(), from.getIdentifiedKey())); + } + if (entry.getGameMode() != -1 && entry.getGameMode() != 256) { + actions.add(UpsertPlayerInfo.Action.UPDATE_GAME_MODE); + playerInfoEntry.setGameMode(entry.getGameMode()); + } + playerInfoEntry.setLatency(entry.getLatency()); + playerInfoEntry.setListed(entry.isListed()); + } + this.connection.write(new UpsertPlayerInfo(actions, List.of(playerInfoEntry))); } @Override public Optional removeEntry(UUID uuid) { - Preconditions.checkNotNull(uuid, "uuid"); - - TabListEntry entry = entries.remove(uuid); - if (entry != null) { - PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry); - connection.write( - new PlayerListItem(PlayerListItem.REMOVE_PLAYER, Collections.singletonList(packetItem))); - } - - return Optional.ofNullable(entry); + this.connection.write(new RemovePlayerInfo(List.of(uuid))); + return Optional.ofNullable(this.entries.remove(uuid)); } @Override public boolean containsEntry(UUID uuid) { - Preconditions.checkNotNull(uuid, "uuid"); - return entries.containsKey(uuid); - } - - /** - * Clears all entries from the tab list. Note that the entries are written with {@link - * MinecraftConnection#delayedWrite(Object)}, so make sure to do an explicit {@link - * MinecraftConnection#flush()}. - */ - public void clearAll() { - Collection listEntries = entries.values(); - if (listEntries.isEmpty()) { - return; - } - List items = new ArrayList<>(listEntries.size()); - for (TabListEntry value : listEntries) { - items.add(PlayerListItem.Item.from(value)); - } - entries.clear(); - connection.delayedWrite(new PlayerListItem(PlayerListItem.REMOVE_PLAYER, items)); + return this.entries.containsKey(uuid); } @Override public Collection getEntries() { - return Collections.unmodifiableCollection(this.entries.values()); + return this.entries.values().stream().map(e -> (TabListEntry) e).collect(Collectors.toList()); + } + + @Override + public void clearAll() { + this.connection.delayedWrite(new RemovePlayerInfo(new ArrayList<>(this.entries.keySet()))); + this.entries.clear(); } @Override public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode) { - return buildEntry(profile, displayName, latency, gameMode, null); + return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, null, true); } @Override - public TabListEntry buildEntry(GameProfile profile, - net.kyori.adventure.text.@Nullable Component displayName, - int latency, int gameMode, @Nullable IdentifiedKey key) { - return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, key); + public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode, + @Nullable IdentifiedKey key) { + return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, null, true); } - /** - * Processes a tab list entry packet from the backend. - * - * @param packet the packet to process - */ - public void processBackendPacket(PlayerListItem packet) { - // Packets are already forwarded on, so no need to do that here - for (PlayerListItem.Item item : packet.getItems()) { - UUID uuid = item.getUuid(); - assert uuid != null : "1.7 tab list entry given to modern tab list handler!"; + @Override + public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode, + @Nullable ChatSession chatSession) { + return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, chatSession, true); + } - if (packet.getAction() != PlayerListItem.ADD_PLAYER && !entries.containsKey(uuid)) { - // Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here - continue; - } - - switch (packet.getAction()) { - case PlayerListItem.ADD_PLAYER: { - // ensure that name and properties are available - String name = item.getName(); - List properties = item.getProperties(); - if (name == null || properties == null) { - throw new IllegalStateException("Got null game profile for ADD_PLAYER"); - } - // Verify key - IdentifiedKey providedKey = item.getPlayerKey(); - Optional connected = proxyServer.getPlayer(uuid); - if (connected.isPresent()) { - IdentifiedKey expectedKey = connected.get().getIdentifiedKey(); - if (providedKey != null) { - if (!Objects.equals(expectedKey, providedKey)) { - throw new IllegalStateException("Server provided incorrect player key in playerlist for " - + name + " UUID: " + uuid); - } - } else { - // Substitute the key - // It shouldn't be propagated to remove the signature. - providedKey = expectedKey; - } - } - - entries.putIfAbsent(item.getUuid(), (VelocityTabListEntry) TabListEntry.builder() - .tabList(this) - .profile(new GameProfile(uuid, name, properties)) - .displayName(item.getDisplayName()) - .latency(item.getLatency()) - .playerKey(providedKey) - .gameMode(item.getGameMode()) - .build()); - break; - } - case PlayerListItem.REMOVE_PLAYER: - entries.remove(uuid); - break; - case PlayerListItem.UPDATE_DISPLAY_NAME: { - VelocityTabListEntry entry = entries.get(uuid); - if (entry != null) { - entry.setDisplayNameInternal(item.getDisplayName()); - } - break; - } - case PlayerListItem.UPDATE_LATENCY: { - VelocityTabListEntry entry = entries.get(uuid); - if (entry != null) { - entry.setLatencyInternal(item.getLatency()); - } - break; - } - case PlayerListItem.UPDATE_GAMEMODE: { - VelocityTabListEntry entry = entries.get(uuid); - if (entry != null) { - entry.setGameModeInternal(item.getGameMode()); - } - break; - } - default: - // Nothing we can do here - break; - } + @Override + public void processUpdate(UpsertPlayerInfo infoPacket) { + for (UpsertPlayerInfo.Entry entry : infoPacket.getEntries()) { + processUpsert(infoPacket.getActions(), entry); } } - void updateEntry(int action, TabListEntry entry) { - if (entries.containsKey(entry.getProfile().getId())) { - PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry); + protected UpsertPlayerInfo.Entry createRawEntry(VelocityTabListEntry entry) { + Preconditions.checkNotNull(entry, "entry"); + Preconditions.checkNotNull(entry.getProfile(), "Profile cannot be null"); + Preconditions.checkNotNull(entry.getProfile().getId(), "Profile ID cannot be null"); + return new UpsertPlayerInfo.Entry(entry.getProfile().getId()); + } - IdentifiedKey selectedKey = packetItem.getPlayerKey(); - Optional existing = proxyServer.getPlayer(entry.getProfile().getId()); - if (existing.isPresent()) { - selectedKey = existing.get().getIdentifiedKey(); - } + protected void emitActionRaw(UpsertPlayerInfo.Action action, UpsertPlayerInfo.Entry entry) { + this.connection.write(new UpsertPlayerInfo(EnumSet.of(action), Collections.singletonList(entry))); + } - if (selectedKey != null - && selectedKey.getKeyRevision().getApplicableTo().contains(connection.getProtocolVersion()) - && Objects.equals(selectedKey.getSignatureHolder(), entry.getProfile().getId())) { - packetItem.setPlayerKey(selectedKey); + private void processUpsert(EnumSet actions, UpsertPlayerInfo.Entry entry) { + Preconditions.checkNotNull(entry.getProfileId(), "Profile ID cannot be null"); + UUID profileId = entry.getProfileId(); + VelocityTabListEntry currentEntry = this.entries.get(profileId); + if (actions.contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { + if (currentEntry == null) { + this.entries.put(profileId, + currentEntry = new VelocityTabListEntry( + this, + entry.getProfile(), + null, + 0, + -1, + null, + true + ) + ); } else { - packetItem.setPlayerKey(null); + logger.debug("Received an add player packet for an existing entry; this does nothing."); } + } else if (currentEntry == null) { + logger.debug( + "Received a partial player before an ADD_PLAYER action; profile could not be built. {}", entry); + return; + } + if (actions.contains(UpsertPlayerInfo.Action.UPDATE_GAME_MODE)) { + currentEntry.setGameMode(entry.getGameMode()); + } + if (actions.contains(UpsertPlayerInfo.Action.UPDATE_LATENCY)) { + currentEntry.setLatency(entry.getLatency()); + } + if (actions.contains(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME)) { + currentEntry.setDisplayName(entry.getDisplayName()); + } + if (actions.contains(UpsertPlayerInfo.Action.INITIALIZE_CHAT)) { + currentEntry.setChatSession(entry.getChatSession()); + } + if (actions.contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { + currentEntry.setListed(entry.isListed()); + } + } - connection.write(new PlayerListItem(action, Collections.singletonList(packetItem))); + @Override + public void processRemove(RemovePlayerInfo infoPacket) { + for (UUID uuid : infoPacket.getProfilesToRemove()) { + this.entries.remove(uuid); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java index 143b014e..d913c25b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java @@ -17,98 +17,108 @@ package com.velocitypowered.proxy.tablist; -import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.player.ChatSession; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; import java.util.Optional; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; public class VelocityTabListEntry implements TabListEntry { - private final VelocityTabList tabList; private final GameProfile profile; - private net.kyori.adventure.text.Component displayName; + private Component displayName; private int latency; private int gameMode; - private @Nullable IdentifiedKey playerKey; + private boolean listed; + private @Nullable ChatSession session; - VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, - net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode, - @Nullable IdentifiedKey playerKey) { + public VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, Component displayName, int latency, + int gameMode, @Nullable ChatSession session, boolean listed) { this.tabList = tabList; this.profile = profile; this.displayName = displayName; this.latency = latency; this.gameMode = gameMode; - this.playerKey = playerKey; + this.session = session; + this.listed = listed; + } + + @Override + public @Nullable ChatSession getChatSession() { + return this.session; } @Override public TabList getTabList() { - return tabList; + return this.tabList; } @Override public GameProfile getProfile() { - return profile; + return this.profile; } @Override - public Optional getDisplayNameComponent() { + public Optional getDisplayNameComponent() { return Optional.ofNullable(displayName); } @Override - public TabListEntry setDisplayName(net.kyori.adventure.text.@Nullable Component displayName) { + public TabListEntry setDisplayName(@Nullable Component displayName) { this.displayName = displayName; - tabList.updateEntry(PlayerListItem.UPDATE_DISPLAY_NAME, this); + UpsertPlayerInfo.Entry upsertEntry = this.tabList.createRawEntry(this); + upsertEntry.setDisplayName(displayName); + this.tabList.emitActionRaw(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME, upsertEntry); return this; } - void setDisplayNameInternal(net.kyori.adventure.text.@Nullable Component displayName) { - this.displayName = displayName; - } - @Override public int getLatency() { - return latency; + return this.latency; } @Override public TabListEntry setLatency(int latency) { this.latency = latency; - tabList.updateEntry(PlayerListItem.UPDATE_LATENCY, this); + UpsertPlayerInfo.Entry upsertEntry = this.tabList.createRawEntry(this); + upsertEntry.setLatency(latency); + this.tabList.emitActionRaw(UpsertPlayerInfo.Action.UPDATE_LATENCY, upsertEntry); return this; } - void setLatencyInternal(int latency) { - this.latency = latency; - } - @Override public int getGameMode() { - return gameMode; + return this.gameMode; } @Override public TabListEntry setGameMode(int gameMode) { this.gameMode = gameMode; - tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this); + UpsertPlayerInfo.Entry upsertEntry = this.tabList.createRawEntry(this); + upsertEntry.setGameMode(gameMode); + this.tabList.emitActionRaw(UpsertPlayerInfo.Action.UPDATE_GAME_MODE, upsertEntry); return this; } - void setGameModeInternal(int gameMode) { - this.gameMode = gameMode; + protected void setChatSession(@Nullable ChatSession session) { + this.session = session; } @Override - public IdentifiedKey getIdentifiedKey() { - return playerKey; + public boolean isListed() { + return listed; } - void setPlayerKeyInternal(IdentifiedKey playerKey) { - this.playerKey = playerKey; + @Override + public VelocityTabListEntry setListed(boolean listed) { + this.listed = listed; + UpsertPlayerInfo.Entry upsertEntry = this.tabList.createRawEntry(this); + upsertEntry.setListed(listed); + this.tabList.emitActionRaw(UpsertPlayerInfo.Action.UPDATE_LISTED, upsertEntry); + return this; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java index bbe163ae..ab1d8f6a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java @@ -22,7 +22,7 @@ import com.velocitypowered.api.util.GameProfile; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; -public class VelocityTabListEntryLegacy extends VelocityTabListEntry { +public class VelocityTabListEntryLegacy extends KeyedVelocityTabListEntry { VelocityTabListEntryLegacy(VelocityTabListLegacy tabList, GameProfile profile, @Nullable Component displayName, int latency, int gameMode) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java index 567ae6bb..48ed518a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java @@ -22,8 +22,8 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem; -import com.velocitypowered.proxy.protocol.packet.PlayerListItem.Item; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.Item; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -32,7 +32,7 @@ import java.util.concurrent.ConcurrentHashMap; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; -public class VelocityTabListLegacy extends VelocityTabList { +public class VelocityTabListLegacy extends KeyedVelocityTabList { private final Map nameMapping = new ConcurrentHashMap<>(); @@ -65,35 +65,35 @@ public class VelocityTabListLegacy extends VelocityTabList { @Override public void clearAll() { for (TabListEntry value : entries.values()) { - connection.delayedWrite(new PlayerListItem(PlayerListItem.REMOVE_PLAYER, - Collections.singletonList(PlayerListItem.Item.from(value)))); + connection.delayedWrite(new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, + Collections.singletonList(LegacyPlayerListItem.Item.from(value)))); } entries.clear(); nameMapping.clear(); } @Override - public void processBackendPacket(PlayerListItem packet) { + public void processLegacy(LegacyPlayerListItem packet) { Item item = packet.getItems().get(0); // Only one item per packet in 1.7 switch (packet.getAction()) { - case PlayerListItem.ADD_PLAYER: + case LegacyPlayerListItem.ADD_PLAYER: if (nameMapping.containsKey(item.getName())) { // ADD_PLAYER also used for updating ping - VelocityTabListEntry entry = entries.get(nameMapping.get(item.getName())); + KeyedVelocityTabListEntry entry = entries.get(nameMapping.get(item.getName())); if (entry != null) { entry.setLatencyInternal(item.getLatency()); } } else { UUID uuid = UUID.randomUUID(); // Use a fake uuid to preserve function of custom entries nameMapping.put(item.getName(), uuid); - entries.put(uuid, (VelocityTabListEntry) TabListEntry.builder() + entries.put(uuid, (KeyedVelocityTabListEntry) TabListEntry.builder() .tabList(this) .profile(new GameProfile(uuid, item.getName(), ImmutableList.of())) .latency(item.getLatency()) .build()); } break; - case PlayerListItem.REMOVE_PLAYER: + case LegacyPlayerListItem.REMOVE_PLAYER: UUID removedUuid = nameMapping.remove(item.getName()); if (removedUuid != null) { entries.remove(removedUuid); @@ -109,11 +109,11 @@ public class VelocityTabListLegacy extends VelocityTabList { void updateEntry(int action, TabListEntry entry) { if (entries.containsKey(entry.getProfile().getId())) { switch (action) { - case PlayerListItem.UPDATE_LATENCY: - case PlayerListItem.UPDATE_DISPLAY_NAME: // Add here because we removed beforehand + case LegacyPlayerListItem.UPDATE_LATENCY: + case LegacyPlayerListItem.UPDATE_DISPLAY_NAME: // Add here because we removed beforehand connection - .write(new PlayerListItem(PlayerListItem.ADD_PLAYER, // ADD_PLAYER also updates ping - Collections.singletonList(PlayerListItem.Item.from(entry)))); + .write(new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, // ADD_PLAYER also updates ping + Collections.singletonList(LegacyPlayerListItem.Item.from(entry)))); break; default: // Can't do anything else