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 76386f47a6..c0d3bbe1d5 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -63,7 +63,7 @@ public enum ProtocolVersion { MINECRAFT_1_19_3(761, "1.19.3"), MINECRAFT_1_19_4(762, "1.19.4"), MINECRAFT_1_20(763, "1.20", "1.20.1"), - MINECRAFT_1_20_2(-1 /*764*/, 144, "1.20.2"); + MINECRAFT_1_20_2(-1 /*764*/, 145, "1.20.2"); private static final int SNAPSHOT_BIT = 30; diff --git a/native/src/main/resources/linux_aarch64/velocity-cipher-ossl11x.so b/native/src/main/resources/linux_aarch64/velocity-cipher-ossl11x.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x.so b/native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/linux_aarch64/velocity-compress.so b/native/src/main/resources/linux_aarch64/velocity-compress.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl10x.so b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl10x.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl11x.so b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl11x.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl30x.so b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl30x.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/linux_x86_64/velocity-compress.so b/native/src/main/resources/linux_x86_64/velocity-compress.so old mode 100755 new mode 100644 diff --git a/native/src/main/resources/macos_arm64/velocity-cipher.dylib b/native/src/main/resources/macos_arm64/velocity-cipher.dylib old mode 100755 new mode 100644 diff --git a/native/src/main/resources/macos_arm64/velocity-compress.dylib b/native/src/main/resources/macos_arm64/velocity-compress.dylib old mode 100755 new mode 100644 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index 9145ff133a..6a845f7b1e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -37,6 +37,7 @@ import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder; @@ -60,6 +61,9 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -78,7 +82,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private final Channel channel; private SocketAddress remoteAddress; private StateRegistry state; - private @Nullable MinecraftSessionHandler sessionHandler; + private Map sessionHandlers; + private @Nullable MinecraftSessionHandler activeSessionHandler; private ProtocolVersion protocolVersion; private @Nullable MinecraftConnectionAssociation association; public final VelocityServer server; @@ -96,12 +101,14 @@ public MinecraftConnection(Channel channel, VelocityServer server) { this.remoteAddress = channel.remoteAddress(); this.server = server; this.state = StateRegistry.HANDSHAKE; + + this.sessionHandlers = new HashMap<>(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.connected(); + if (activeSessionHandler != null) { + activeSessionHandler.connected(); } if (association != null && server.getConfiguration().isLogPlayerConnections()) { @@ -111,12 +118,12 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.disconnected(); + if (activeSessionHandler != null) { + activeSessionHandler.disconnected(); } if (association != null && !knownDisconnect - && !(sessionHandler instanceof StatusSessionHandler) + && !(activeSessionHandler instanceof StatusSessionHandler) && server.getConfiguration().isLogPlayerConnections()) { logger.info("{} has disconnected", association); } @@ -125,12 +132,12 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { - if (sessionHandler == null) { + if (activeSessionHandler == null) { // No session handler available, do nothing return; } - if (sessionHandler.beforeHandle()) { + if (activeSessionHandler.beforeHandle()) { return; } @@ -140,15 +147,15 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception if (msg instanceof MinecraftPacket) { MinecraftPacket pkt = (MinecraftPacket) msg; - if (!pkt.handle(sessionHandler)) { - sessionHandler.handleGeneric((MinecraftPacket) msg); + if (!pkt.handle(activeSessionHandler)) { + activeSessionHandler.handleGeneric((MinecraftPacket) msg); } } else if (msg instanceof HAProxyMessage) { HAProxyMessage proxyMessage = (HAProxyMessage) msg; this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); } else if (msg instanceof ByteBuf) { - sessionHandler.handleUnknown((ByteBuf) msg); + activeSessionHandler.handleUnknown((ByteBuf) msg); } } finally { ReferenceCountUtil.release(msg); @@ -157,20 +164,20 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.readCompleted(); + if (activeSessionHandler != null) { + activeSessionHandler.readCompleted(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (ctx.channel().isActive()) { - if (sessionHandler != null) { + if (activeSessionHandler != null) { try { - sessionHandler.exception(cause); + activeSessionHandler.exception(cause); } catch (Exception ex) { logger.error("{}: exception handling exception in {}", - (association != null ? association : channel.remoteAddress()), sessionHandler, cause); + (association != null ? association : channel.remoteAddress()), activeSessionHandler, cause); } } @@ -178,13 +185,13 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E if (cause instanceof ReadTimeoutException) { logger.error("{}: read timed out", association); } else { - boolean frontlineHandler = sessionHandler instanceof InitialLoginSessionHandler - || sessionHandler instanceof HandshakeSessionHandler - || sessionHandler instanceof StatusSessionHandler; + boolean frontlineHandler = activeSessionHandler instanceof InitialLoginSessionHandler + || activeSessionHandler instanceof HandshakeSessionHandler + || activeSessionHandler instanceof StatusSessionHandler; boolean isQuietDecoderException = cause instanceof QuietDecoderException; boolean willLog = !isQuietDecoderException && !frontlineHandler; if (willLog) { - logger.error("{}: exception encountered in {}", association, sessionHandler, cause); + logger.error("{}: exception encountered in {}", association, activeSessionHandler, cause); } else { knownDisconnect = true; } @@ -197,8 +204,8 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.writabilityChanged(); + if (activeSessionHandler != null) { + activeSessionHandler.writabilityChanged(); } } @@ -341,12 +348,8 @@ public void setAutoReading(boolean autoReading) { } } - /** - * Changes the state of the Minecraft connection. - * - * @param state the new state - */ - public void setState(StateRegistry state) { + // Ideally only used by the state switch + private void setState(StateRegistry state) { ensureInEventLoop(); this.state = state; @@ -382,25 +385,73 @@ public void setProtocolVersion(ProtocolVersion protocolVersion) { } } - public @Nullable MinecraftSessionHandler getSessionHandler() { - return sessionHandler; + public @Nullable MinecraftSessionHandler getActiveSessionHandler() { + return activeSessionHandler; + } + + public @Nullable MinecraftSessionHandler getSessionHandlerForRegistry(StateRegistry registry) { + return this.sessionHandlers.getOrDefault(registry, null); } /** * Sets the session handler for this connection. * + * @param registry the registry of the handler * @param sessionHandler the handler to use */ - public void setSessionHandler(MinecraftSessionHandler sessionHandler) { + public void setActiveSessionHandler(StateRegistry registry, MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); ensureInEventLoop(); - if (this.sessionHandler != null) { - this.sessionHandler.deactivated(); + if (this.activeSessionHandler != null) { + this.activeSessionHandler.deactivated(); } - this.sessionHandler = sessionHandler; + this.sessionHandlers.put(registry, sessionHandler); + this.activeSessionHandler = sessionHandler; + setState(registry); sessionHandler.activated(); } + /** + * Switches the active session handler to the respective registry one. + * + * @param registry the registry of the handler + * @return true if successful and handler is present + */ + public boolean setActiveSessionHandler(StateRegistry registry) { + Preconditions.checkNotNull(registry); + ensureInEventLoop(); + + MinecraftSessionHandler handler = getSessionHandlerForRegistry(registry); + if (handler != null) { + boolean flag = true; + if (this.activeSessionHandler != null + && (flag = !Objects.equals(handler, this.activeSessionHandler))) { + this.activeSessionHandler.deactivated(); + } + this.activeSessionHandler = handler; + setState(registry); + if (flag) { + handler.activated(); + } + } + return handler != null; + } + + /** + * Adds a secondary session handler for this connection. + * + * @param registry the registry of the handler + * @param sessionHandler the handler to use + */ + public void addSessionHandler(StateRegistry registry, MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); + Preconditions.checkArgument(registry != state, "Handler would overwrite handler"); + ensureInEventLoop(); + + this.sessionHandlers.put(registry, sessionHandler); + } + private void ensureOpen() { Preconditions.checkState(!isClosed(), "Connection is closed."); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConfigSessionHandler.java deleted file mode 100644 index 95a899a708..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConfigSessionHandler.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2018-2023 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.connection.backend; - -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.proxy.VelocityServer; -import com.velocitypowered.proxy.config.PlayerInfoForwarding; -import com.velocitypowered.proxy.config.VelocityConfiguration; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.connection.VelocityConstants; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; -import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.packet.Disconnect; -import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; -import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; -import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; -import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; -import com.velocitypowered.proxy.protocol.packet.SetCompression; -import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; -import com.velocitypowered.proxy.util.except.QuietRuntimeException; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import net.kyori.adventure.text.Component; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.CompletableFuture; - -/** - * Handles the initial server configuration setup - */ -public class BackendConfigSessionHandler implements MinecraftSessionHandler { - - private static final Logger logger = LogManager.getLogger(BackendConfigSessionHandler.class); - - private final VelocityServer server; - private final VelocityServerConnection serverConn; - private final CompletableFuture resultFuture; - private boolean informationForwarded; - - BackendConfigSessionHandler(VelocityServer server, VelocityServerConnection serverConn, - CompletableFuture resultFuture) { - this.server = server; - this.serverConn = serverConn; - this.resultFuture = resultFuture; - } - - - @Override - public boolean handle(StartUpdate packet) { - - return true; - } - -} 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 3adf6ba25a..79b27f40ca 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 @@ -66,7 +66,6 @@ */ 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 .getBoolean("velocity.log-server-backpressure"); @@ -86,7 +85,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { this.serverConn = serverConn; this.playerConnection = serverConn.getPlayer().getConnection(); - MinecraftSessionHandler psh = playerConnection.getSessionHandler(); + MinecraftSessionHandler psh = playerConnection.getActiveSessionHandler(); if (!(psh instanceof ClientPlaySessionHandler)) { throw new IllegalStateException( "Initializing BackendPlaySessionHandler with no backing client play session handler!"); @@ -103,9 +102,10 @@ public void activated() { if (server.getConfiguration().isBungeePluginChannelEnabled()) { MinecraftConnection serverMc = serverConn.ensureConnected(); - serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), - ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())) - )); + if (serverMc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) + serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), + ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())) + )); } } @@ -144,21 +144,8 @@ public boolean handle(BossBar packet) { @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); - - String hash = packet.getHash(); - if (hash != null && !hash.isEmpty()) { - if (PLAUSIBLE_SHA1_HASH.matcher(hash).matches()) { - builder.setHash(ByteBufUtil.decodeHexDump(hash)); - } - } - ServerResourcePackSendEvent event = new ServerResourcePackSendEvent( - builder.build(), this.serverConn); + packet.toServerPromptedPack(), this.serverConn); server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackSendEvent -> { if (playerConnection.isClosed()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java new file mode 100644 index 0000000000..bd12b83939 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2019-2023 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.connection.backend; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.util.ConnectionMessages; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.Disconnect; +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.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import jdk.jfr.Experimental; +import net.kyori.adventure.text.Component; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +/** + * A special session handler that catches "last minute" disconnects. + * This version is to accommodate 1.20.2+ switching. + * Yes, some of this is exceptionally stupid. + */ +@Experimental +public class ConfigSessionHandler implements MinecraftSessionHandler { + + private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class); + + private final VelocityServer server; + private final VelocityServerConnection serverConn; + private final CompletableFuture resultFuture; + + + private State state; + + /** + * Creates the new transition handler. + * + * @param server the Velocity server instance + * @param serverConn the server connection + * @param resultFuture the result future + */ + ConfigSessionHandler(VelocityServer server, + VelocityServerConnection serverConn, + CompletableFuture resultFuture) { + this.server = server; + this.serverConn = serverConn; + this.resultFuture = resultFuture; + this.state = State.START; + + } + + @Override + public void deactivated() { + } + + @Override + public boolean beforeHandle() { + if (!serverConn.isActive()) { + // Obsolete connection + serverConn.disconnect(); + return true; + } + return false; + } + + @Override + public boolean handle(KeepAlive packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(ResourcePackRequest packet) { + final MinecraftConnection playerConnection = serverConn.getPlayer().getConnection(); + + ServerResourcePackSendEvent event = new ServerResourcePackSendEvent( + packet.toServerPromptedPack(), this.serverConn); + + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackSendEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackSendEvent.getResult().isAllowed()) { + ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); + if (toSend != serverResourcePackSendEvent.getReceivedResourcePack()) { + ((VelocityResourcePackInfo) toSend) + .setOriginalOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + } + + serverConn.getPlayer().queueResourcePack(toSend); + } else if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse( + packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED + )); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse( + packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED + )); + } + logger.error("Exception while handling resource pack send for {}", playerConnection, ex); + return null; + }); + + return true; + } + + @Override + public boolean handle(FinishedUpdate packet) { + + ClientConfigSessionHandler configHandler = + (ClientConfigSessionHandler) serverConn.getPlayer().getConnection().getActiveSessionHandler(); + + configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { + serverConn.ensureConnected().write(new FinishedUpdate()); + serverConn.ensureConnected().setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + }, serverConn.ensureConnected().eventLoop()); + return true; + } + + + private void switchFailure(Throwable cause) { + logger.error("Unable to switch to new server {} for {}", + serverConn.getServerInfo().getName(), + serverConn.getPlayer().getUsername(), cause); + serverConn.getPlayer().disconnect(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + resultFuture.completeExceptionally(cause); + } +/* + private ClientConfigSessionHandler getClientConfigSessionHandler(ConnectedPlayer player) { + ClientPlaySessionHandler playHandler; + if (player.getConnection().getSessionHandler() instanceof ClientPlaySessionHandler) { + playHandler = (ClientPlaySessionHandler) player.getConnection().getSessionHandler(); + } else { + playHandler = new ClientPlaySessionHandler(server, player); + } + + ClientConfigSessionHandler configHandler; + if (player.getConnection().getSessionHandler() instanceof ClientConfigSessionHandler) { + configHandler = (ClientConfigSessionHandler) player.getConnection().getSessionHandler(); + } else { + configHandler = new ClientConfigSessionHandler(server, player, playHandler); + } + return configHandler; + } +*/ + @Override + public boolean handle(Disconnect packet) { + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer())); + return true; + } + + @Override + public boolean handle(PluginMessage packet) { + if (PluginMessageUtil.isMcBrand(packet)) { + serverConn.getPlayer().getConnection().write( + PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), + serverConn.getPlayer().getProtocolVersion())); + } else { + // TODO: Change this so its usable for mod loaders + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect( + Component.translatable("multiplayer.disconnect.missing_tags"), serverConn.getServer())); + } + return true; + } + + @Override + public void disconnected() { + resultFuture + .completeExceptionally(new IOException("Unexpectedly disconnected from remote server")); + } + + @Override + public boolean handle(RegistrySync packet) { + serverConn.getPlayer().getConnection().write(packet.retain()); + return true; + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + serverConn.getPlayer().getConnection().write(packet); + } + + public static enum State{ + START, + NEGOTIATING, + PLUGIN_MESSAGE_INTERRUPT, + RESOURCE_PACK_INTERRUPT, + COMPLETE + } +} 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 90a9a1300b..2710b0f1d7 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 @@ -27,6 +27,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -34,6 +35,7 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; @@ -150,10 +152,23 @@ public boolean handle(ServerLoginSuccess packet) { // Move into the PLAY phase. MinecraftConnection smc = serverConn.ensureConnected(); - smc.setState(StateRegistry.PLAY); + if (smc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); + } else { + CompletableFuture switchFuture; + if (serverConn.getPlayer().getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + switchFuture = ((ClientPlaySessionHandler) serverConn.getPlayer().getConnection().getActiveSessionHandler()) + .doSwitch(); + } else { + switchFuture = CompletableFuture.completedFuture(null); + } + switchFuture.thenAcceptAsync((unused) -> { + smc.write(new LoginAcknowledged()); + //Sync backend + smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture)); + }, smc.eventLoop()); + } - // Switch to the transition handler. - smc.setSessionHandler(new TransitionSessionHandler(server, serverConn, resultFuture)); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 25cbbef639..d83d07dcc6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -32,6 +32,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; @@ -62,8 +63,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { * @param resultFuture the result future */ TransitionSessionHandler(VelocityServer server, - VelocityServerConnection serverConn, - CompletableFuture resultFuture) { + VelocityServerConnection serverConn, + CompletableFuture resultFuture) { this.server = server; this.serverConn = serverConn; this.resultFuture = resultFuture; @@ -121,17 +122,18 @@ public boolean handle(JoinGame packet) { // Change the client to use the ClientPlaySessionHandler if required. ClientPlaySessionHandler playHandler; - if (player.getConnection().getSessionHandler() instanceof ClientPlaySessionHandler) { - playHandler = (ClientPlaySessionHandler) player.getConnection().getSessionHandler(); + if (player.getConnection().setActiveSessionHandler(StateRegistry.PLAY)) { + playHandler = (ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler(); } else { playHandler = new ClientPlaySessionHandler(server, player); - player.getConnection().setSessionHandler(playHandler); + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler); } + assert playHandler != null; playHandler.handleBackendJoinGame(packet, serverConn); // Set the new play session handler for the server. We will have nothing more to do // with this connection once this task finishes up. - smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn)); + smc.setActiveSessionHandler(StateRegistry.PLAY, new BackendPlaySessionHandler(server, serverConn)); // Clean up disabling auto-read while the connected event was being processed. smc.setAutoReading(true); 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 a2300b9780..0ffa87cce7 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 @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -108,8 +109,11 @@ public CompletableFuture connect() { future.channel().pipeline().addLast(HANDLER, connection); // Kick off the connection process - connection.setSessionHandler( - new LoginSessionHandler(server, VelocityServerConnection.this, result)); + if (!connection.setActiveSessionHandler(StateRegistry.HANDSHAKE)) { + MinecraftSessionHandler handler = new LoginSessionHandler(server, VelocityServerConnection.this, result); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler); + connection.addSessionHandler(StateRegistry.LOGIN, handler); + } // Set the connection phase, which may, for future forge (or whatever), be determined // at this point already @@ -193,7 +197,7 @@ private void startHandshake() { mc.delayedWrite(handshake); mc.setProtocolVersion(protocolVersion); - mc.setState(StateRegistry.LOGIN); + mc.setActiveSessionHandler(StateRegistry.LOGIN); if (proxyPlayer.getIdentifiedKey() == null && proxyPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getUniqueId())); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index f68d3bb595..e0116a65da 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -38,8 +39,10 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import io.netty.buffer.ByteBuf; import java.util.Objects; import java.util.Optional; @@ -64,6 +67,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private GameProfile profile; private @MonotonicNonNull ConnectedPlayer connectedPlayer; private final boolean onlineMode; + private State loginState = State.START; // 1.20.2+ AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode) { @@ -118,7 +122,7 @@ public void activated() { } else { player.setPermissionFunction(function); } - completeLoginProtocolPhaseAndInitialize(player); + startLoginCompletion(player); } }, mcConnection.eventLoop()); }, mcConnection.eventLoop()).exceptionally((ex) -> { @@ -127,7 +131,7 @@ public void activated() { }); } - private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { + private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { mcConnection.write(new SetCompression(threshold)); @@ -171,8 +175,27 @@ private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { success.setUuid(playerUniqueId); mcConnection.write(success); + if (inbound.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + completeLoginProtocolPhaseAndInitialize(player); + } else { + loginState = State.SUCCESS_SENT; + } + } + + @Override + public boolean handle(LoginAcknowledged packet) { + if (loginState != State.SUCCESS_SENT) { + inbound.disconnect( + Component.translatable("multiplayer.disconnect.invalid_player_data")); + } else { + loginState = State.ACKNOWLEDGED; + completeLoginProtocolPhaseAndInitialize(connectedPlayer); + } + return true; + } + + private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { mcConnection.setAssociation(player); - mcConnection.setState(StateRegistry.PLAY); server.getEventManager().fire(new LoginEvent(player)) .thenAcceptAsync(event -> { @@ -193,7 +216,12 @@ private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { return; } - mcConnection.setSessionHandler(new InitialConnectSessionHandler(player, server)); + if (inbound.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) > 0) { + mcConnection.setActiveSessionHandler(StateRegistry.PLAY, new InitialConnectSessionHandler(player, server)); + } else { + mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, player)); + } + server.getEventManager().fire(new PostLoginEvent(player)) .thenCompose((ignored) -> connectToInitialServer(player)) .exceptionally((ex) -> { @@ -237,4 +265,10 @@ public void disconnected() { } this.inbound.cleanup(); } + + static enum State { + START, + SUCCESS_SENT, + ACKNOWLEDGED + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java new file mode 100644 index 0000000000..84658cf40f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2018-2023 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.connection.client; + +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import io.netty.buffer.ByteBuf; +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; + +import java.util.concurrent.CompletableFuture; + +public class ClientConfigSessionHandler implements MinecraftSessionHandler { + + private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class); + private final VelocityServer server; + private final ConnectedPlayer player; + + private CompletableFuture configSwitchFuture; + + + /** + * Constructs a client config session handler. + * + * @param server the Velocity server instance + * @param player the player + */ + public ClientConfigSessionHandler(VelocityServer server, ConnectedPlayer player) { + this.server = server; + this.player = player; + + } + + @Override + public void activated() { + configSwitchFuture = new CompletableFuture<>(); + } + + @Override + public void deactivated() { + + } + + @Override + public boolean handle(KeepAlive packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + player.setPing(System.currentTimeMillis() - sentTime); + smc.write(packet); + } + } + } + return true; + } + + + @Override + public boolean handle(PluginMessage packet) { + + + return true; + } + + @Override + public boolean handle(ResourcePackResponse packet) { + + + return true; + } + + @Override + public boolean handle(FinishedUpdate packet) { + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, + new ClientPlaySessionHandler(server, player)); + + configSwitchFuture.complete(null); + + return true; + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && serverConnection.getPhase().consideredComplete()) { + if (packet instanceof PluginMessage) { + ((PluginMessage) packet).retain(); + } + smc.write(packet); + } + } + + @Override + public void handleUnknown(ByteBuf buf) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { + smc.write(buf.retain()); + } + } + + @Override + public void disconnected() { + player.teardown(); + } + + @Override + public void exception(Throwable throwable) { + player.disconnect( + Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); + } + + public CompletableFuture handleBackendFinishUpdate(VelocityServerConnection serverConn) { + player.getConnection().write(new FinishedUpdate()); + return configSwitchFuture; + } + +} 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 bc041d8ab3..737b5b324d 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 @@ -17,11 +17,10 @@ package com.velocitypowered.proxy.connection.client; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; +import static com.velocitypowered.api.network.ProtocolVersion.*; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.mojang.brigadier.suggestion.Suggestion; import com.velocitypowered.api.command.VelocityBrigadierMessage; @@ -67,6 +66,7 @@ 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.config.StartUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; @@ -80,6 +80,7 @@ import java.util.List; 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; @@ -105,6 +106,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final CommandHandler commandHandler; private final ChatTimeKeeper timeKeeper = new ChatTimeKeeper(); + private CompletableFuture configSwitchFuture; + /** * Constructs a client play session handler. * @@ -151,6 +154,7 @@ private boolean validateChat(String message) { @Override public void activated() { + configSwitchFuture = new CompletableFuture<>(); Collection channels = server.getChannelRegistrar() .getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { @@ -380,6 +384,14 @@ public boolean handle(ResourcePackResponse packet) { return player.onResourcePackResponse(packet.getStatus()); } + @Override + public boolean handle(StartUpdate packet) { + //Complete client switch + player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); + configSwitchFuture.complete(null); + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); @@ -443,6 +455,26 @@ public void writabilityChanged() { } } + public CompletableFuture doSwitch() { + VelocityServerConnection existingConnection = player.getConnectedServer(); + + if (existingConnection != null) { + // Shut down the existing server connection. + player.setConnectedServer(null); + existingConnection.disconnect(); + + // Send keep alive to try to avoid timeouts + player.sendKeepAlive(); + + // Reset Tablist header and footer to prevent desync + player.clearHeaderAndFooter(); + } + + player.getConnection().write(new StartUpdate()); + return configSwitchFuture; + } + + /** * Handles the {@code JoinGame} packet. This function is responsible for handling the client-side * switching servers in Velocity. @@ -487,7 +519,7 @@ public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection de // Tell the server about this client's plugin message channels. ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - if (!player.getKnownChannels().isEmpty()) { + if (!player.getKnownChannels().isEmpty() && serverVersion.compareTo(MINECRAFT_1_20_2) < 0) { serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels())); } 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 579cd60ee7..bca066e250 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 @@ -1072,6 +1072,13 @@ public boolean onResourcePackResponse(PlayerResourcePackStatusEvent.Status statu && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; } + /** + * Gives an indication about the previous resource pack responses. + */ + public @Nullable Boolean getPreviousResourceResponse() { + return previousResourceResponse; + } + /** * Sends a {@link KeepAlive} packet to the player with a random ID. The response will be ignored * by Velocity as it will not match the ID last sent by the server. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index e814efaa35..6dc99558ec 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -67,7 +67,7 @@ public boolean handle(LegacyPing packet) { connection.setProtocolVersion(ProtocolVersion.LEGACY); StatusSessionHandler handler = new StatusSessionHandler(server, new LegacyInboundConnection(connection, packet)); - connection.setSessionHandler(handler); + connection.setActiveSessionHandler(StateRegistry.STATUS ,handler); handler.handle(packet); return true; } @@ -90,13 +90,12 @@ public boolean handle(Handshake handshake) { LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus()); connection.close(true); } else { - connection.setState(nextState); connection.setProtocolVersion(handshake.getProtocolVersion()); connection.setAssociation(ic); switch (nextState) { case STATUS: - connection.setSessionHandler(new StatusSessionHandler(server, ic)); + connection.setActiveSessionHandler(StateRegistry.STATUS, new StatusSessionHandler(server, ic)); break; case LOGIN: this.handleLogin(handshake, ic); @@ -147,7 +146,7 @@ private void handleLogin(Handshake handshake, InitialInboundConnection ic) { LoginInboundConnection lic = new LoginInboundConnection(ic); server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(lic)); - connection.setSessionHandler(new InitialLoginSessionHandler(server, connection, lic)); + connection.setActiveSessionHandler(StateRegistry.LOGIN, new InitialLoginSessionHandler(server, connection, lic)); } private ConnectionType getHandshakeConnectionType(Handshake handshake) { 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 01bef5f92c..366149c76a 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 @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; import com.velocitypowered.proxy.protocol.packet.EncryptionResponse; @@ -150,7 +151,7 @@ public boolean handle(ServerLogin packet) { mcConnection.write(request); this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; } else { - mcConnection.setSessionHandler(new AuthSessionHandler( + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, new AuthSessionHandler( server, inbound, GameProfile.forOfflinePlayer(login.getUsername()), false )); } @@ -246,7 +247,7 @@ public boolean handle(EncryptionResponse packet) { } } // All went well, initialize the session. - mcConnection.setSessionHandler(new AuthSessionHandler( + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, new AuthSessionHandler( server, inbound, profile, true )); } else if (profileResponse.getStatusCode() == 204) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java index 5d75421708..063bd3ec9b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java @@ -165,7 +165,7 @@ boolean onHandle(ConnectedPlayer player, // just in case the timing is awful player.sendKeepAlive(); - MinecraftSessionHandler handler = backendConn.getSessionHandler(); + MinecraftSessionHandler handler = backendConn.getActiveSessionHandler(); if (handler instanceof ClientPlaySessionHandler) { ((ClientPlaySessionHandler) handler).flushQueuedMessages(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java new file mode 100644 index 0000000000..2647ed14ea --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018-2023 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.connection.registry; + +import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import org.jetbrains.annotations.Nullable; + +public class ClientConfigData { + + private final @Nullable VelocityResourcePackInfo resourcePackInfo; + private final DataTag tag; + private final RegistrySync registry; + private final Key[] features; + private final String brand; + + + private ClientConfigData(@Nullable VelocityResourcePackInfo resourcePackInfo, DataTag tag, + RegistrySync registry, Key[] features, String brand) { + this.resourcePackInfo = resourcePackInfo; + this.tag = tag; + this.registry = registry; + this.features = features; + this.brand = brand; + } + + + public RegistrySync getRegistry() { + return registry; + } + + public DataTag getTag() { + return tag; + } + + public Key[] getFeatures() { + return features; + } + + public @Nullable VelocityResourcePackInfo getResourcePackInfo() { + return resourcePackInfo; + } + + public String getBrand() { + return brand; + } + + public static ClientConfigData.Builder builder(){ + return new Builder(); + } + + + public static class Builder { + + private VelocityResourcePackInfo resourcePackInfo; + private DataTag tag; + private RegistrySync registry; + private Key[] features; + private String brand; + + private Builder(){ + } + + public void clear(){ + this.resourcePackInfo = null; + this.tag = null; + this.registry = null; + this.features = null; + this.brand = null; + } + + public Builder resourcePack(@Nullable VelocityResourcePackInfo resourcePackInfo) { + this.resourcePackInfo = resourcePackInfo; + return this; + } + + public Builder dataTag(DataTag tag) { + this.tag = tag; + return this; + } + + public Builder registry(RegistrySync registry) { + this.registry = registry; + return this; + } + + public Builder features(Key[] features) { + this.features = features; + return this; + } + + public Builder brand(String brand) { + this.brand = brand; + return this; + } + + public ClientConfigData build(){ + return new ClientConfigData(resourcePackInfo, tag, registry, features, brand); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java new file mode 100644 index 0000000000..edc4ecc8b3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2019-2023 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.connection.registry; + +import com.google.common.collect.ImmutableList; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class DataTag { + + + private final ImmutableList entrySets; + + public DataTag(ImmutableList entrySets) { + this.entrySets = entrySets; + } + + public List getEntrySets() { + return entrySets; + } + + public static class Set implements Keyed { + + private final Key key; + private final ImmutableList entries; + + public Set(Key key, ImmutableList entries) { + this.key = key; + this.entries = entries; + } + + public List getEntries() { + return entries; + } + + @Override + public @NotNull Key key() { + return key; + } + } + + public static class Entry implements Keyed { + + private final Key key; + private final int[] elements; + + public Entry(Key key, int[] elements) { + this.key = key; + this.elements = elements; + } + + public int[] getElements() { + return elements; + } + + @Override + public @NotNull Key key() { + return key; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java index 62f38ebd72..50ddd52b45 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java @@ -29,6 +29,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; @@ -67,7 +68,7 @@ protected void initChannel(final Channel ch) { .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND)); final MinecraftConnection connection = new MinecraftConnection(ch, this.server); - connection.setSessionHandler(new HandshakeSessionHandler(connection, this.server)); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, new HandshakeSessionHandler(connection, this.server)); ch.pipeline().addLast(Connections.HANDLER, connection); if (this.server.getConfiguration().isProxyProtocol()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index ebd349cf01..b6b07c7a3e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -41,6 +41,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; + +import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -219,6 +221,52 @@ public static void writeString(ByteBuf buf, CharSequence str) { buf.writeCharSequence(str, StandardCharsets.UTF_8); } + /** + * Reads a standard Mojang Text namespaced:key from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key + */ + public static Key readKey(ByteBuf buf) { + return Key.key(readString(buf), Key.DEFAULT_SEPARATOR); + } + + /** + * Writes a standard Mojang Text namespaced:key to the buffer. + * + * @param buf the buffer to write to + * @param key the key to write + */ + public static void writeKey(ByteBuf buf, Key key) { + writeString(buf, key.asString()); + } + + /** + * Reads a standard Mojang Text namespaced:key array from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key array + */ + public static Key[] readKeyArray(ByteBuf buf) { + Key[] ret = new Key[readVarInt(buf)]; + for (int i = 0; i < ret.length; i++) { + ret[i] = ProtocolUtils.readKey(buf); + } + return ret; + } + + /** + * Writes a standard Mojang Text namespaced:key array to the buffer. + * + * @param buf the buffer to write to + * @param keys the keys to write + */ + public static void writeKeyArray(ByteBuf buf, Key[] keys) { + writeVarInt(buf, keys.length); + for (Key key : keys) { + writeKey(buf, key); + } + } public static byte[] readByteArray(ByteBuf buf) { return readByteArray(buf, DEFAULT_MAX_STRING_SIZE); } 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 d91bdaf7ec..b506c94356 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -178,7 +178,8 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19, false), map(0x09, MINECRAFT_1_19_1, false), map(0x08, MINECRAFT_1_19_3, false), - map(0x09, MINECRAFT_1_19_4, false)); + map(0x09, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); serverbound.register(LegacyChat.class, LegacyChat::new, map(0x01, MINECRAFT_1_7_2, false), map(0x02, MINECRAFT_1_9, false), @@ -204,7 +205,8 @@ public enum StateRegistry { map(0x07, MINECRAFT_1_19, false), map(0x08, MINECRAFT_1_19_1, false), map(0x07, MINECRAFT_1_19_3, false), - map(0x08, MINECRAFT_1_19_4, false)); + map(0x08, MINECRAFT_1_19_4, false), + map(0x09, MINECRAFT_1_20_2, false)); serverbound.register(PluginMessage.class, PluginMessage::new, map(0x17, MINECRAFT_1_7_2, false), map(0x09, MINECRAFT_1_9, false), @@ -242,8 +244,8 @@ public enum StateRegistry { map(0x21, MINECRAFT_1_16_2, false), map(0x23, MINECRAFT_1_19, false), map(0x24, MINECRAFT_1_19_1, false), - map(0x26, MINECRAFT_1_20_2, false)); - serverbound.register(FinishedUpdate.class, FinishedUpdate::new, + map(0x27, MINECRAFT_1_20_2, false)); + serverbound.register(StartUpdate.class, StartUpdate::new, map(0x0B, MINECRAFT_1_20_2, false)); clientbound.register(BossBar.class, BossBar::new, @@ -353,7 +355,7 @@ public enum StateRegistry { map(0x3E, MINECRAFT_1_19_1, true), map(0x3D, MINECRAFT_1_19_3, true), map(0x41, MINECRAFT_1_19_4, true), - map(0x43, MINECRAFT_1_20_2, true)); + map(0x44, MINECRAFT_1_20_2, true)); clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, false), map(0x32, MINECRAFT_1_9, false), @@ -369,7 +371,7 @@ public enum StateRegistry { map(0x3D, MINECRAFT_1_19_1, false), map(0x3C, MINECRAFT_1_19_3, false), map(0x40, MINECRAFT_1_19_4, false), - map(0x42, MINECRAFT_1_20_2, false)); + map(0x43, MINECRAFT_1_20_2, false)); clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), @@ -386,7 +388,7 @@ public enum StateRegistry { map(0x63, MINECRAFT_1_19_1, true), map(0x61, MINECRAFT_1_19_3, true), map(0x65, MINECRAFT_1_19_4, true), - map(0x68, MINECRAFT_1_20_2, true)); + map(0x69, MINECRAFT_1_20_2, true)); clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), @@ -402,28 +404,28 @@ public enum StateRegistry { map(0x5B, MINECRAFT_1_19_1, true), map(0x59, MINECRAFT_1_19_3, true), map(0x5D, MINECRAFT_1_19_4, true), - map(0x5F, MINECRAFT_1_20_2, true)); + map(0x60, MINECRAFT_1_20_2, 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(0x5B, MINECRAFT_1_19_3, true), map(0x5F, MINECRAFT_1_19_4, true), - map(0x61, MINECRAFT_1_20_2, true)); + map(0x62, MINECRAFT_1_20_2, 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(0x42, MINECRAFT_1_19_3, true), map(0x46, MINECRAFT_1_19_4, true), - map(0x48, MINECRAFT_1_20_2, true)); + map(0x49, MINECRAFT_1_20_2, 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(0x5C, MINECRAFT_1_19_3, true), map(0x60, MINECRAFT_1_19_4, true), - map(0x62, MINECRAFT_1_20_2, true)); + map(0x63, MINECRAFT_1_20_2, true)); clientbound.register(TitleClearPacket.class, TitleClearPacket::new, map(0x10, MINECRAFT_1_17, true), map(0x0D, MINECRAFT_1_19, true), @@ -445,17 +447,17 @@ public enum StateRegistry { clientbound.register(RemovePlayerInfo.class, RemovePlayerInfo::new, map(0x35, MINECRAFT_1_19_3, false), map(0x39, MINECRAFT_1_19_4, false), - map(0x3B, MINECRAFT_1_20_2, false)); + map(0x3C, MINECRAFT_1_20_2, false)); clientbound.register(UpsertPlayerInfo.class, UpsertPlayerInfo::new, map(0x36, MINECRAFT_1_19_3, false), map(0x3A, MINECRAFT_1_19_4, false), - map(0x3B, MINECRAFT_1_20_2, false)); + map(0x3D, MINECRAFT_1_20_2, false)); clientbound.register(SystemChat.class, SystemChat::new, map(0x5F, MINECRAFT_1_19, true), map(0x62, MINECRAFT_1_19_1, true), map(0x60, MINECRAFT_1_19_3, true), map(0x64, MINECRAFT_1_19_4, true), - map(0x67, MINECRAFT_1_20_2, true)); + map(0x68, MINECRAFT_1_20_2, true)); clientbound.register(PlayerChatCompletion.class, PlayerChatCompletion::new, map(0x15, MINECRAFT_1_19_1, true), map(0x14, MINECRAFT_1_19_3, true), @@ -466,9 +468,9 @@ public enum StateRegistry { map(0x42, MINECRAFT_1_19_1, false), map(0x41, MINECRAFT_1_19_3, false), map(0x45, MINECRAFT_1_19_4, false), - map(0x47, MINECRAFT_1_20_2, false)); + map(0x48, MINECRAFT_1_20_2, false)); clientbound.register(StartUpdate.class, StartUpdate::new, - map(0x65, MINECRAFT_1_20_2, false)); + map(0x66, MINECRAFT_1_20_2, false)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index cfbecce319..876f6bcae6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -202,9 +202,11 @@ public String toString() { @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, - // so separate it out. + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // Minecraft 1.16, 1.20.1 and above have significantly more complicated logic for reading this packet, + // so separate them out. + this.decode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { this.decode116Up(buf, version); } else { this.decodeLegacy(buf, version); @@ -295,11 +297,45 @@ private void decode116Up(ByteBuf buf, ProtocolVersion version) { } } + private void decode1202Up(ByteBuf buf, ProtocolVersion version) { + this.entityId = buf.readInt(); + this.isHardcore = buf.readBoolean(); + + this.levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); + + this.maxPlayers = ProtocolUtils.readVarInt(buf); + + this.viewDistance = ProtocolUtils.readVarInt(buf); + this.simulationDistance = ProtocolUtils.readVarInt(buf); + + this.reducedDebugInfo = buf.readBoolean(); + this.showRespawnScreen = buf.readBoolean(); + + String dimensionIdentifier = ProtocolUtils.readString(buf); + String levelName = ProtocolUtils.readString(buf); + this.partialHashedSeed = buf.readLong(); + + this.gamemode = buf.readByte(); + this.previousGamemode = buf.readByte(); + + boolean isDebug = buf.readBoolean(); + boolean isFlat = buf.readBoolean(); + this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); + + // optional death location + if (buf.readBoolean()) + this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); + + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } + @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, - // so separate it out. + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // Minecraft 1.16, 1.20.2 and above have significantly more complicated logic for reading this packet, + // so separate them out. + this.encode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0){ this.encode116Up(buf, version); } else { this.encodeLegacy(buf, version); @@ -396,6 +432,42 @@ private void encode116Up(ByteBuf buf, ProtocolVersion version) { } } + private void encode1202Up(ByteBuf buf, ProtocolVersion version) { + buf.writeInt(entityId); + buf.writeBoolean(isHardcore); + + ProtocolUtils.writeStringArray(buf, levelNames.toArray(String[]::new)); + + ProtocolUtils.writeVarInt(buf, maxPlayers); + + ProtocolUtils.writeVarInt(buf, viewDistance); + ProtocolUtils.writeVarInt(buf, simulationDistance); + + buf.writeBoolean(reducedDebugInfo); + buf.writeBoolean(showRespawnScreen); + + ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); + ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); + buf.writeLong(partialHashedSeed); + + buf.writeByte(gamemode); + buf.writeByte(previousGamemode); + + buf.writeBoolean(dimensionInfo.isDebugType()); + buf.writeBoolean(dimensionInfo.isFlat()); + + // optional death location + if (lastDeathPosition != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, lastDeathPosition.key()); + buf.writeLong(lastDeathPosition.value()); + } else { + buf.writeBoolean(false); + } + + ProtocolUtils.writeVarInt(buf, portalCooldown); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java index 7f90a433d6..f901a4d557 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018-2023 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; @@ -8,22 +25,22 @@ public class LoginAcknowledged implements MinecraftPacket { - @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - } + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + } - @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - } + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + } - @Override - public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { return 0; } - @Override - public boolean handle(MinecraftSessionHandler handler) { + @Override + public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java index 641407f9f5..5278d79df1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java @@ -17,17 +17,23 @@ package com.velocitypowered.proxy.protocol.packet; +import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.regex.Pattern; + public class ResourcePackRequest implements MinecraftPacket { private @MonotonicNonNull String url; @@ -35,6 +41,8 @@ public class ResourcePackRequest implements MinecraftPacket { private boolean isRequired; // 1.17+ private @Nullable Component prompt; // 1.17+ + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); + public @Nullable String getUrl() { return url; } @@ -99,6 +107,21 @@ public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVer } } + public VelocityResourcePackInfo toServerPromptedPack(){ + ResourcePackInfo.Builder builder = new VelocityResourcePackInfo.BuilderImpl( + Preconditions.checkNotNull(url)) + .setPrompt(prompt) + .setShouldForce(isRequired) + .setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + + if (hash != null && !hash.isEmpty()) { + if (PLAUSIBLE_SHA1_HASH.matcher(hash).matches()) { + builder.setHash(ByteBufUtil.decodeHexDump(hash)); + } + } + return (VelocityResourcePackInfo) builder.build(); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); 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 0cd589ee9e..bb9588ec24 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 @@ -99,7 +99,10 @@ public void decode(ByteBuf buf, Direction direction, ProtocolVersion version) { } if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { - if (buf.readBoolean()) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + holderUuid = ProtocolUtils.readUuid(buf); + } + else if (buf.readBoolean()) { holderUuid = ProtocolUtils.readUuid(buf); } } @@ -130,7 +133,8 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi buf.writeBoolean(true); ProtocolUtils.writeUuid(buf, playerKey.getSignatureHolder()); } else if (this.holderUuid != null) { - buf.writeBoolean(true); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) + buf.writeBoolean(true); ProtocolUtils.writeUuid(buf, this.holderUuid); } else { buf.writeBoolean(false); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java index ee5b02bf7c..6ca4628691 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java @@ -22,27 +22,36 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; public class ActiveFeatures implements MinecraftPacket { - private String[] activeFeatures; + private Key[] activeFeatures; + + public ActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } public ActiveFeatures(){ - this.activeFeatures = new String[0]; + this.activeFeatures = new Key[0]; } - public ActiveFeatures(String[] activeFeatures) { + public void setActiveFeatures(Key[] activeFeatures) { this.activeFeatures = activeFeatures; } + public Key[] getActiveFeatures() { + return activeFeatures; + } + @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - activeFeatures = ProtocolUtils.readStringArray(buf); + activeFeatures = ProtocolUtils.readKeyArray(buf); } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - ProtocolUtils.writeStringArray(buf, activeFeatures); + ProtocolUtils.writeKeyArray(buf, activeFeatures); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java index 38c45ab137..d0cea74bf3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java @@ -21,22 +21,27 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; -public class RegistrySync implements MinecraftPacket { +public class RegistrySync extends DeferredByteBufHolder implements MinecraftPacket { - private CompoundBinaryTag registryData; + public RegistrySync() { + super(null); + } + //NBT change in 1.20.2 makes it difficult to parse this packet. @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - registryData = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader()); + this.replace(buf.readRetainedSlice(buf.readableBytes())); } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - ProtocolUtils.writeCompoundTag(buf, registryData); + buf.writeBytes(content()); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java index fe7527c92f..15a2df0b4d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java @@ -17,58 +17,71 @@ package com.velocitypowered.proxy.protocol.packet.config; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.registry.DataTag; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; +import java.util.List; import java.util.Map; public class TagsUpdate implements MinecraftPacket { - private Map> tags; + private DataTag tag; - public TagsUpdate(Map> tags) { - this.tags = tags; + public TagsUpdate(DataTag tag) { + this.tag = tag; } public TagsUpdate() { - this.tags = Map.of(); + this.tag = new DataTag(ImmutableList.of()); + } + + public DataTag getTag() { + return tag; + } + + public void setTag(DataTag tag) { + this.tag = tag; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - ImmutableMap.Builder> builder = ImmutableMap.builder(); + ImmutableList.Builder entrySet = ImmutableList.builder(); + int size = ProtocolUtils.readVarInt(buf); for (int i = 0; i < size; i++) { - String key = ProtocolUtils.readString(buf); + Key setkey = ProtocolUtils.readKey(buf); int innerSize = ProtocolUtils.readVarInt(buf); - ImmutableMap.Builder innerBuilder = ImmutableMap.builder(); + ImmutableList.Builder innerBuilder = ImmutableList.builder(); for (int j = 0; j < innerSize; j++) { - String innerKey = ProtocolUtils.readString(buf); - int[] innerValue = ProtocolUtils.readVarIntArray(buf); - innerBuilder.put(innerKey, innerValue); + innerBuilder.add(new DataTag.Entry( + ProtocolUtils.readKey(buf), + ProtocolUtils.readVarIntArray(buf)) + ); } - builder.put(key, innerBuilder.build()); + entrySet.add(new DataTag.Set(setkey, innerBuilder.build())); } - tags = builder.build(); + tag = new DataTag(entrySet.build()); } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - ProtocolUtils.writeVarInt(buf, tags.size()); - for (Map.Entry> entry : tags.entrySet()) { - ProtocolUtils.writeString(buf, entry.getKey()); - // Oh, joy - ProtocolUtils.writeVarInt(buf, entry.getValue().size()); - for (Map.Entry innerEntry : entry.getValue().entrySet()) { - // Yea, object oriented programming be damned - ProtocolUtils.writeString(buf, innerEntry.getKey()); - ProtocolUtils.writeVarIntArray(buf, innerEntry.getValue()); + ProtocolUtils.writeVarInt(buf, tag.getEntrySets().size()); + for (DataTag.Set set : tag.getEntrySets()) { + ProtocolUtils.writeKey(buf, set.key()); + + ProtocolUtils.writeVarInt(buf, set.getEntries().size()); + for (DataTag.Entry entry : set.getEntries()) { + ProtocolUtils.writeKey(buf, entry.key()); + ProtocolUtils.writeVarIntArray(buf, entry.getElements()); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 7bafc99b7d..9cfec28d21 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -60,7 +60,7 @@ public void activated() { handshake.setProtocolVersion(version); connection.delayedWrite(handshake); - connection.setState(StateRegistry.STATUS); + connection.setActiveSessionHandler(StateRegistry.STATUS); connection.delayedWrite(StatusRequest.INSTANCE); connection.flush(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index 57780110b0..bf8a18bc79 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -37,10 +37,12 @@ import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.packet.Handshake; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; @@ -129,7 +131,7 @@ protected void initChannel(Channel ch) throws Exception { .addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); - conn.setSessionHandler(new PingSessionHandler( + conn.setActiveSessionHandler(StateRegistry.HANDSHAKE ,new PingSessionHandler( pingFuture, VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion())); } else { pingFuture.completeExceptionally(future.cause());