diff --git a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java index 831e55af20..4e4718267e 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java @@ -114,6 +114,13 @@ public interface ProxyConfig { */ int getCompressionLevel(); + /** + * Get the minimum packet size required to check whether a packet can be passed as is. + * + * @return the decompression threshold + */ + int getDecompressionThreshold(); + /** * Get the limit for how long a player must wait to log back in. * diff --git a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java index 7182b352f5..aa60cde64b 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java @@ -78,6 +78,33 @@ public void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize) } } + @Override + public void inflatePartial(ByteBuf source, ByteBuf destination, int size) + throws DataFormatException { + ensureNotDisposed(); + + // We (probably) can't nicely deal with >=1 buffer nicely, so let's scream loudly. + checkArgument(source.nioBufferCount() == 1, "source has multiple backing buffers"); + checkArgument(destination.nioBufferCount() == 1, "destination has multiple backing buffers"); + + final int origReaderIdx = source.readerIndex(); + + destination.ensureWritable(size); + + try { + while (!inflater.finished() && size > 0) { + inflater.setInput(source.nioBuffer()); + int produced = inflater.inflate( + destination.nioBuffer(destination.writerIndex(), size)); + size -= produced; + destination.writerIndex(destination.writerIndex() + produced); + source.readerIndex(origReaderIdx + inflater.getTotalIn()); + } + } finally { + inflater.reset(); + } + } + @Override public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException { ensureNotDisposed(); diff --git a/native/src/main/java/com/velocitypowered/natives/compression/VelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/VelocityCompressor.java index 393104983f..dc2abada2b 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/VelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/VelocityCompressor.java @@ -30,5 +30,10 @@ public interface VelocityCompressor extends Disposable, Native { void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize) throws DataFormatException; + default void inflatePartial(ByteBuf source, ByteBuf destination, int size) + throws DataFormatException { + throw new UnsupportedOperationException(); + } + void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 3afd9fc107..582034b830 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -323,6 +323,11 @@ public int getCompressionLevel() { return advanced.getCompressionLevel(); } + @Override + public int getDecompressionThreshold() { + return advanced.getDecompressionThreshold(); + } + @Override public int getLoginRatelimit() { return advanced.getLoginRatelimit(); @@ -737,6 +742,8 @@ private static class Advanced { @Expose private int compressionLevel = -1; @Expose + private int decompressionThreshold = 2048; + @Expose private int loginRatelimit = 3000; @Expose private int connectionTimeout = 5000; @@ -766,6 +773,7 @@ private Advanced(CommentedConfig config) { if (config != null) { this.compressionThreshold = config.getIntOrElse("compression-threshold", 256); this.compressionLevel = config.getIntOrElse("compression-level", -1); + this.decompressionThreshold = config.getIntOrElse("decompression-threshold", 2048); this.loginRatelimit = config.getIntOrElse("login-ratelimit", 3000); this.connectionTimeout = config.getIntOrElse("connection-timeout", 5000); this.readTimeout = config.getIntOrElse("read-timeout", 30000); @@ -793,6 +801,10 @@ public int getCompressionLevel() { return compressionLevel; } + public int getDecompressionThreshold() { + return decompressionThreshold; + } + public int getLoginRatelimit() { return loginRatelimit; } @@ -842,6 +854,7 @@ public String toString() { return "Advanced{" + "compressionThreshold=" + compressionThreshold + ", compressionLevel=" + compressionLevel + + ", decompressionThreshold=" + decompressionThreshold + ", loginRatelimit=" + loginRatelimit + ", connectionTimeout=" + connectionTimeout + ", readTimeout=" + readTimeout 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 bb0c4db725..52f2d24dd5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -24,10 +24,11 @@ import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; +import static com.velocitypowered.proxy.network.Connections.MINECRAFT_PRE_ENCODER; import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.natives.compression.JavaVelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.encryption.VelocityCipher; import com.velocitypowered.natives.encryption.VelocityCipherFactory; @@ -42,14 +43,13 @@ import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherEncoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftCompressDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftCompressAndIdDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftPreEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler; import com.velocitypowered.proxy.util.except.QuietDecoderException; -import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; @@ -155,8 +155,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception HAProxyMessage proxyMessage = (HAProxyMessage) msg; this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); - } else if (msg instanceof ByteBuf) { - activeSessionHandler.handleUnknown((ByteBuf) msg); + } else { + activeSessionHandler.handleUnknown(msg); } } finally { ReferenceCountUtil.release(msg); @@ -362,7 +362,7 @@ public void setState(StateRegistry state) { ensureInEventLoop(); this.state = state; - this.channel.pipeline().get(MinecraftEncoder.class).setState(state); + this.channel.pipeline().get(MinecraftPreEncoder.class).setState(state); this.channel.pipeline().get(MinecraftDecoder.class).setState(state); if (state == StateRegistry.CONFIG) { @@ -379,9 +379,9 @@ public void setState(StateRegistry state) { */ public void addPlayPacketQueueHandler() { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { - this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, + this.channel.pipeline().addAfter(MINECRAFT_PRE_ENCODER, Connections.PLAY_PACKET_QUEUE, new PlayPacketQueueHandler(this.protocolVersion, - channel.pipeline().get(MinecraftEncoder.class).getDirection())); + channel.pipeline().get(MinecraftPreEncoder.class).getDirection())); } } @@ -400,11 +400,11 @@ public void setProtocolVersion(ProtocolVersion protocolVersion) { boolean changed = this.protocolVersion != protocolVersion; this.protocolVersion = protocolVersion; if (protocolVersion != ProtocolVersion.LEGACY) { - this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion); + this.channel.pipeline().get(MinecraftPreEncoder.class).setProtocolVersion(protocolVersion); this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion); } else { // Legacy handshake handling - this.channel.pipeline().remove(MINECRAFT_ENCODER); + this.channel.pipeline().remove(MINECRAFT_PRE_ENCODER); this.channel.pipeline().remove(MINECRAFT_DECODER); } @@ -496,16 +496,17 @@ public void setCompressionThreshold(int threshold) { ensureInEventLoop(); if (threshold == -1) { - final ChannelHandler removedDecoder = channel.pipeline().remove(COMPRESSION_DECODER); + channel.pipeline().replace(COMPRESSION_DECODER, COMPRESSION_DECODER, + new MinecraftCompressAndIdDecoder(server)); final ChannelHandler removedEncoder = channel.pipeline().remove(COMPRESSION_ENCODER); - if (removedDecoder != null && removedEncoder != null) { + if (removedEncoder != null) { channel.pipeline().addBefore(MINECRAFT_DECODER, FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE); channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_DISABLED); } } else { - MinecraftCompressDecoder decoder = (MinecraftCompressDecoder) channel.pipeline() + MinecraftCompressAndIdDecoder decoder = (MinecraftCompressAndIdDecoder) channel.pipeline() .get(COMPRESSION_DECODER); MinecraftCompressorAndLengthEncoder encoder = (MinecraftCompressorAndLengthEncoder) channel.pipeline().get(COMPRESSION_ENCODER); @@ -517,11 +518,12 @@ public void setCompressionThreshold(int threshold) { VelocityCompressor compressor = Natives.compress.get().create(level); encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor); - decoder = new MinecraftCompressDecoder(threshold, compressor); + decoder = new MinecraftCompressAndIdDecoder(threshold, compressor, + JavaVelocityCompressor.FACTORY.create(1), server); channel.pipeline().remove(FRAME_ENCODER); - channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); - channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); + channel.pipeline().replace(COMPRESSION_DECODER, COMPRESSION_DECODER, decoder); + channel.pipeline().addBefore(MINECRAFT_PRE_ENCODER, COMPRESSION_ENCODER, encoder); channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED); } 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 4e8af7f68d..237dd6232a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -70,7 +70,6 @@ import com.velocitypowered.proxy.protocol.packet.title.TitleSubtitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleTextPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleTimesPacket; -import io.netty.buffer.ByteBuf; /** * Interface for dispatching received Minecraft packets. @@ -85,7 +84,7 @@ default void handleGeneric(MinecraftPacket packet) { } - default void handleUnknown(ByteBuf buf) { + default void handleUnknown(Object obj) { } 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 c34b6fa7cd..4a7e7e6c08 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 @@ -57,7 +57,6 @@ import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; @@ -334,8 +333,8 @@ public void handleGeneric(MinecraftPacket packet) { } @Override - public void handleUnknown(ByteBuf buf) { - playerConnection.delayedWrite(buf.retain()); + public void handleUnknown(Object obj) { + playerConnection.delayedWrite(obj); if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { playerConnection.flush(); packetsFlushed = 0; 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 603cf686a6..b51e8397c6 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 @@ -42,7 +42,6 @@ import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; -import io.netty.buffer.ByteBuf; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -251,7 +250,7 @@ private CompletableFuture connectToInitialServer(ConnectedPlayer player) { } @Override - public void handleUnknown(ByteBuf buf) { + public void handleUnknown(Object obj) { mcConnection.close(true); } 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 index 8281b40e94..6746359ac3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -155,7 +155,7 @@ public void handleGeneric(MinecraftPacket packet) { } @Override - public void handleUnknown(ByteBuf buf) { + public void handleUnknown(Object obj) { VelocityServerConnection serverConnection = player.getConnectedServer(); if (serverConnection == null) { // No server connection yet, probably transitioning. @@ -164,7 +164,7 @@ public void handleUnknown(ByteBuf buf) { MinecraftConnection smc = serverConnection.getConnection(); if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { - smc.write(buf.retain()); + smc.write(obj); } } 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 08da36ab53..d5f92f47ce 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 @@ -69,7 +69,6 @@ import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.ReferenceCountUtil; @@ -420,7 +419,7 @@ public void handleGeneric(MinecraftPacket packet) { } @Override - public void handleUnknown(ByteBuf buf) { + public void handleUnknown(Object obj) { VelocityServerConnection serverConnection = player.getConnectedServer(); if (serverConnection == null) { // No server connection yet, probably transitioning. @@ -429,7 +428,7 @@ public void handleUnknown(ByteBuf buf) { MinecraftConnection smc = serverConnection.getConnection(); if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { - smc.write(buf.retain()); + smc.write(obj); } } 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 5bd49bd9e2..4305355f1e 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 @@ -58,7 +58,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftPreEncoder; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; @@ -1129,7 +1129,7 @@ public void switchToConfigState() { CompletableFuture.runAsync(() -> { connection.write(new StartUpdate()); connection.getChannel().pipeline() - .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); + .get(MinecraftPreEncoder.class).setState(StateRegistry.CONFIG); // Make sure we don't send any play packets to the player after update start connection.addPlayPacketQueueHandler(); }, connection.eventLoop()).exceptionally((ex) -> { 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 079ce035f8..9905c3b6a0 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 @@ -35,7 +35,6 @@ import com.velocitypowered.proxy.protocol.packet.LegacyDisconnect; import com.velocitypowered.proxy.protocol.packet.LegacyHandshake; import com.velocitypowered.proxy.protocol.packet.LegacyPing; -import io.netty.buffer.ByteBuf; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Optional; @@ -198,7 +197,7 @@ public void handleGeneric(MinecraftPacket packet) { } @Override - public void handleUnknown(ByteBuf buf) { + public void handleUnknown(Object obj) { // Unknown packet received. Better to close the connection. connection.close(true); } 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 1409dc4bad..04a770b0a0 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 @@ -40,7 +40,6 @@ import com.velocitypowered.proxy.protocol.packet.EncryptionResponse; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; import com.velocitypowered.proxy.protocol.packet.ServerLogin; -import io.netty.buffer.ByteBuf; import java.net.InetSocketAddress; import java.security.GeneralSecurityException; import java.security.KeyPair; @@ -284,7 +283,7 @@ private EncryptionRequest generateEncryptionRequest() { } @Override - public void handleUnknown(ByteBuf buf) { + public void handleUnknown(Object obj) { mcConnection.close(true); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index ada5700799..a12b10097a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -28,7 +28,6 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.util.except.QuietRuntimeException; -import io.netty.buffer.ByteBuf; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -109,7 +108,7 @@ public boolean handle(StatusRequest packet) { } @Override - public void handleUnknown(ByteBuf buf) { + public void handleUnknown(Object obj) { // what even is going on? connection.close(true); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java index 1e6f387b39..0807406ca6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java @@ -17,18 +17,20 @@ package com.velocitypowered.proxy.network; +import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER; import static com.velocitypowered.proxy.network.Connections.FLOW_HANDLER; import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; +import static com.velocitypowered.proxy.network.Connections.MINECRAFT_PRE_ENCODER; import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.netty.AutoReadHolderHandler; +import com.velocitypowered.proxy.protocol.netty.MinecraftCompressAndIdDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftPreEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import io.netty.channel.Channel; @@ -56,10 +58,11 @@ protected void initChannel(Channel ch) throws Exception { new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.MILLISECONDS)) .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(COMPRESSION_DECODER, new MinecraftCompressAndIdDecoder(server)) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) .addLast(FLOW_HANDLER, new AutoReadHolderHandler()) - .addLast(MINECRAFT_ENCODER, - new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); + .addLast(MINECRAFT_PRE_ENCODER, + new MinecraftPreEncoder(ProtocolUtils.Direction.SERVERBOUND)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index 27ec4ba8b8..bddc31bdf1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -33,7 +33,7 @@ public class Connections { public static final String LEGACY_PING_DECODER = "legacy-ping-decoder"; public static final String LEGACY_PING_ENCODER = "legacy-ping-encoder"; public static final String MINECRAFT_DECODER = "minecraft-decoder"; - public static final String MINECRAFT_ENCODER = "minecraft-encoder"; + public static final String MINECRAFT_PRE_ENCODER = "minecraft-pre-encoder"; public static final String READ_TIMEOUT = "read-timeout"; public static final String PLAY_PACKET_QUEUE = "play-packet-queue"; 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 ef8e6b1cbf..50b4becd1b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java @@ -17,12 +17,13 @@ package com.velocitypowered.proxy.network; +import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER; import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; import static com.velocitypowered.proxy.network.Connections.LEGACY_PING_DECODER; import static com.velocitypowered.proxy.network.Connections.LEGACY_PING_ENCODER; import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; +import static com.velocitypowered.proxy.network.Connections.MINECRAFT_PRE_ENCODER; import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; import com.velocitypowered.proxy.VelocityServer; @@ -32,8 +33,9 @@ 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.MinecraftCompressAndIdDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftPreEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import io.netty.channel.Channel; @@ -64,8 +66,10 @@ protected void initChannel(final Channel ch) { TimeUnit.MILLISECONDS)) .addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE) .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(COMPRESSION_DECODER, new MinecraftCompressAndIdDecoder(this.server)) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.SERVERBOUND)) - .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND)); + .addLast(MINECRAFT_PRE_ENCODER, + new MinecraftPreEncoder(ProtocolUtils.Direction.CLIENTBOUND)); final MinecraftConnection connection = new MinecraftConnection(ch, this.server); connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressAndIdDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressAndIdDecoder.java new file mode 100644 index 0000000000..6073dcee65 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressAndIdDecoder.java @@ -0,0 +1,124 @@ +/* + * 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.netty; + +import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible; +import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer; +import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; + +import com.velocitypowered.natives.compression.VelocityCompressor; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.netty.data.CompressedPacket; +import com.velocitypowered.proxy.protocol.netty.data.UncompressedPacket; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import java.util.List; + +/** + * Decompresses a Minecraft packet and decodes id. + */ +public class MinecraftCompressAndIdDecoder extends MessageToMessageDecoder { + + private int threshold; + private final VelocityCompressor compressor; + private final VelocityCompressor javaCompressor; + private final VelocityServer server; + + /** + * Constructs new Minecraft packet decompressor and id decoder. + * + * @param threshold Compression threshold. + * @param compressor Preferred compressor. + * @param javaCompressor Java compressor for partial decompression. + */ + public MinecraftCompressAndIdDecoder(int threshold, VelocityCompressor compressor, + VelocityCompressor javaCompressor, VelocityServer server) { + this.threshold = threshold; + this.compressor = compressor; + this.javaCompressor = javaCompressor; + this.server = server; + } + + public MinecraftCompressAndIdDecoder(VelocityServer server) { + this(0, null, null, server); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (threshold <= 0) { + int originalReaderIndex = in.readerIndex(); + int packetId = ProtocolUtils.readVarInt(in); + out.add(new UncompressedPacket(packetId, in.readerIndex(originalReaderIndex).retain())); + return; + } + + int claimedUncompressedSize = ProtocolUtils.readVarInt(in); + if (claimedUncompressedSize == 0) { + int originalReaderIndex = in.readerIndex(); + int packetId = ProtocolUtils.readVarInt(in); + out.add(new UncompressedPacket(packetId, in.readerIndex(originalReaderIndex).retain())); + return; + } + + checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than" + + " threshold %s", claimedUncompressedSize, threshold); + + if (claimedUncompressedSize < this.server.getConfiguration().getDecompressionThreshold()) { + ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), this.compressor, in); + ByteBuf uncompressed = preferredBuffer(ctx.alloc(), this.compressor, claimedUncompressedSize); + try { + this.compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); + int originalReaderIndex = uncompressed.readerIndex(); + int packetId = ProtocolUtils.readVarInt(uncompressed); + out.add(new UncompressedPacket(packetId, uncompressed.readerIndex(originalReaderIndex))); + } catch (Exception e) { + uncompressed.release(); + throw e; + } finally { + compatibleIn.release(); + } + } else { + ByteBuf packetIdBuf = preferredBuffer(ctx.alloc(), this.javaCompressor, 5); + int readerIndex = in.readerIndex(); + this.javaCompressor.inflatePartial(in, packetIdBuf, 5); + in.readerIndex(readerIndex); + int packetId = ProtocolUtils.readVarInt(packetIdBuf); + packetIdBuf.release(); + + out.add(new CompressedPacket( + packetId, claimedUncompressedSize, in.retain(), this.compressor)); + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + if (compressor != null) { + compressor.close(); + } + + if (javaCompressor != null) { + javaCompressor.close(); + } + } + + public void setThreshold(int threshold) { + this.threshold = threshold; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java deleted file mode 100644 index 6e7fb4d4e8..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ /dev/null @@ -1,87 +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.protocol.netty; - -import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible; -import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer; -import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; - -import com.velocitypowered.natives.compression.VelocityCompressor; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageDecoder; -import java.util.List; - -/** - * Decompresses a Minecraft packet. - */ -public class MinecraftCompressDecoder extends MessageToMessageDecoder { - - private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB - private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB - - private static final int UNCOMPRESSED_CAP = - Boolean.getBoolean("velocity.increased-compression-cap") - ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; - - private int threshold; - private final VelocityCompressor compressor; - - public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) { - this.threshold = threshold; - this.compressor = compressor; - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - int claimedUncompressedSize = ProtocolUtils.readVarInt(in); - if (claimedUncompressedSize == 0) { - // This message is not compressed. - out.add(in.retain()); - return; - } - - checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than" - + " threshold %s", claimedUncompressedSize, threshold); - checkFrame(claimedUncompressedSize <= UNCOMPRESSED_CAP, - "Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize, - UNCOMPRESSED_CAP); - - ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in); - ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize); - try { - compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); - out.add(uncompressed); - } catch (Exception e) { - uncompressed.release(); - throw e; - } finally { - compatibleIn.release(); - } - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - compressor.close(); - } - - public void setThreshold(int threshold) { - this.threshold = threshold; - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java index 90952a729b..55ed057f4c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java @@ -22,6 +22,9 @@ import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.util.MoreByteBufUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.netty.data.CompressedPacket; +import com.velocitypowered.proxy.protocol.netty.data.IdentifiedPacket; +import com.velocitypowered.proxy.protocol.netty.data.UncompressedPacket; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; @@ -30,7 +33,7 @@ /** * Handler for compressing Minecraft packets. */ -public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder { +public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder { private int threshold; private final VelocityCompressor compressor; @@ -41,36 +44,46 @@ public MinecraftCompressorAndLengthEncoder(int threshold, VelocityCompressor com } @Override - protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { - int uncompressed = msg.readableBytes(); - if (uncompressed < threshold) { - // Under the threshold, there is nothing to do. - ProtocolUtils.writeVarInt(out, uncompressed + 1); - ProtocolUtils.writeVarInt(out, 0); - out.writeBytes(msg); - } else { - handleCompressed(ctx, msg, out); + protected void encode(ChannelHandlerContext ctx, IdentifiedPacket msg, ByteBuf out) + throws Exception { + if (msg instanceof UncompressedPacket) { + UncompressedPacket uncompressed = (UncompressedPacket) msg; + int uncompressedLength = uncompressed.getPacketBuf().readableBytes(); + if (uncompressedLength < threshold || threshold <= 0) { + // Under the threshold, there is nothing to do. + ProtocolUtils.writeVarInt(out, uncompressedLength + 1); + ProtocolUtils.writeVarInt(out, 0); + out.writeBytes(uncompressed.getPacketBuf()); + uncompressed.getPacketBuf().release(); + } else { + handleCompressed(ctx, uncompressed, out); + } + } else if (msg instanceof CompressedPacket) { + CompressedPacket compressed = (CompressedPacket) msg; + if (compressed.getUncompressedLength() < threshold || threshold <= 0) { + ProtocolUtils.writeVarInt(out, compressed.getUncompressedLength() + 1); + ProtocolUtils.writeVarInt(out, 0); + ByteBuf decompressed = compressed.decompress(ctx.alloc()); + out.writeBytes(decompressed); + decompressed.release(); + } else { + ProtocolUtils.writeVarInt(out, compressed.getCompressedBuf().readableBytes() + + ProtocolUtils.varIntBytes(compressed.getUncompressedLength())); + ProtocolUtils.writeVarInt(out, compressed.getUncompressedLength()); + out.writeBytes(compressed.getCompressedBuf()); + compressed.getCompressedBuf().release(); + } } } - private void handleCompressed(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) + private void handleCompressed(ChannelHandlerContext ctx, UncompressedPacket msg, ByteBuf out) throws DataFormatException { - int uncompressed = msg.readableBytes(); + int uncompressed = msg.getPacketBuf().readableBytes(); ProtocolUtils.write21BitVarInt(out, 0); // Dummy packet length ProtocolUtils.writeVarInt(out, uncompressed); - ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg); - int startCompressed = out.writerIndex(); - try { - compressor.deflate(compatibleIn, out); - } finally { - compatibleIn.release(); - } - int compressedLength = out.writerIndex() - startCompressed; - if (compressedLength >= 1 << 21) { - throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet."); - } + msg.compress(this.compressor, ctx.alloc(), out); int writerIndex = out.writerIndex(); int packetLength = out.readableBytes() - 3; @@ -80,9 +93,16 @@ private void handleCompressed(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf ou } @Override - protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) - throws Exception { - int uncompressed = msg.readableBytes(); + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, IdentifiedPacket msg, + boolean preferDirect) throws Exception { + int uncompressed; + if (msg instanceof UncompressedPacket) { + uncompressed = ((UncompressedPacket) msg).getPacketBuf().readableBytes(); + } else if (msg instanceof CompressedPacket) { + uncompressed = ((CompressedPacket) msg).getUncompressedLength(); + } else { + throw new IllegalArgumentException("Unsupported identified packet type."); + } if (uncompressed < threshold) { int finalBufferSize = uncompressed + 1; finalBufferSize += ProtocolUtils.varIntBytes(finalBufferSize); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java index f8362cc093..74b30a9b0a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java @@ -22,16 +22,20 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.data.CompressedPacket; +import com.velocitypowered.proxy.protocol.netty.data.IdentifiedPacket; +import com.velocitypowered.proxy.protocol.netty.data.UncompressedPacket; import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.MessageToMessageDecoder; +import java.util.List; /** * Decodes Minecraft packets. */ -public class MinecraftDecoder extends ChannelInboundHandlerAdapter { +public class MinecraftDecoder extends MessageToMessageDecoder { public static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging"); private static final QuietRuntimeException DECODE_FAILED = @@ -55,43 +59,45 @@ public MinecraftDecoder(ProtocolUtils.Direction direction) { } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof ByteBuf) { - ByteBuf buf = (ByteBuf) msg; - tryDecode(ctx, buf); - } else { + protected void decode(ChannelHandlerContext ctx, IdentifiedPacket msg, List out) + throws Exception { + int packetId = msg.getPacketId(); + MinecraftPacket packet = registry.createPacket(packetId); + if (packet == null) { ctx.fireChannelRead(msg); - } - } + } else { + ByteBuf uncompressedBuf; + if (msg instanceof UncompressedPacket) { + uncompressedBuf = ((UncompressedPacket) msg).getPacketBuf(); + } else if (msg instanceof CompressedPacket) { + uncompressedBuf = ((CompressedPacket) msg).decompress(ctx.alloc()); + } else { + throw new IllegalArgumentException("Unsupported identified packet type."); + } - private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception { - if (!ctx.channel().isActive() || !buf.isReadable()) { - buf.release(); - return; - } + if (!ctx.channel().isActive() || !uncompressedBuf.isReadable()) { + uncompressedBuf.release(); + return; + } - int originalReaderIndex = buf.readerIndex(); - int packetId = ProtocolUtils.readVarInt(buf); - MinecraftPacket packet = this.registry.createPacket(packetId); - if (packet == null) { - buf.readerIndex(originalReaderIndex); - ctx.fireChannelRead(buf); - } else { try { - doLengthSanityChecks(buf, packet); + ProtocolUtils.readVarInt(uncompressedBuf); + doLengthSanityChecks(uncompressedBuf, packet); try { - packet.decode(buf, direction, registry.version); + packet.decode(uncompressedBuf, direction, registry.version); } catch (Exception e) { + e.printStackTrace(); throw handleDecodeFailure(e, packet, packetId); } - if (buf.isReadable()) { - throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex()); + if (uncompressedBuf.isReadable()) { + throw handleOverflow(packet, + uncompressedBuf.readerIndex(), uncompressedBuf.writerIndex()); } ctx.fireChannelRead(packet); } finally { - buf.release(); + uncompressedBuf.release(); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftPreEncoder.java similarity index 80% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftPreEncoder.java index 7133f1d26a..e3b1254a9f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftPreEncoder.java @@ -22,14 +22,16 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.data.UncompressedPacket; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.handler.codec.MessageToMessageEncoder; +import java.util.List; /** * Encodes {@link MinecraftPacket} instances. */ -public class MinecraftEncoder extends MessageToByteEncoder { +public class MinecraftPreEncoder extends MessageToMessageEncoder { private final ProtocolUtils.Direction direction; private StateRegistry state; @@ -40,7 +42,7 @@ public class MinecraftEncoder extends MessageToByteEncoder { * * @param direction the direction to encode to */ - public MinecraftEncoder(ProtocolUtils.Direction direction) { + public MinecraftPreEncoder(ProtocolUtils.Direction direction) { this.direction = Preconditions.checkNotNull(direction, "direction"); this.registry = StateRegistry.HANDSHAKE.getProtocolRegistry( direction, ProtocolVersion.MINIMUM_VERSION); @@ -48,10 +50,12 @@ public MinecraftEncoder(ProtocolUtils.Direction direction) { } @Override - protected void encode(ChannelHandlerContext ctx, MinecraftPacket msg, ByteBuf out) { + protected void encode(ChannelHandlerContext ctx, MinecraftPacket msg, List out) { int packetId = this.registry.getPacketId(msg); - ProtocolUtils.writeVarInt(out, packetId); - msg.encode(out, direction, registry.version); + ByteBuf buf = ctx.alloc().buffer(); + ProtocolUtils.writeVarInt(buf, packetId); + msg.encode(buf, direction, registry.version); + out.add(new UncompressedPacket(packetId, buf)); } public void setProtocolVersion(final ProtocolVersion protocolVersion) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java index ecbcc8b848..6343161903 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java @@ -20,6 +20,7 @@ import com.velocitypowered.natives.encryption.JavaVelocityCipher; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.netty.data.UncompressedPacket; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; @@ -29,7 +30,7 @@ * Handler for appending a length for Minecraft packets. */ @ChannelHandler.Sharable -public class MinecraftVarintLengthEncoder extends MessageToByteEncoder { +public class MinecraftVarintLengthEncoder extends MessageToByteEncoder { public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder(); public static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY; @@ -38,16 +39,18 @@ private MinecraftVarintLengthEncoder() { } @Override - protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { - ProtocolUtils.writeVarInt(out, msg.readableBytes()); - out.writeBytes(msg); + protected void encode(ChannelHandlerContext ctx, UncompressedPacket msg, ByteBuf out) + throws Exception { + ProtocolUtils.writeVarInt(out, msg.getPacketBuf().readableBytes()); + out.writeBytes(msg.getPacketBuf()); + msg.getPacketBuf().release(); } @Override - protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) - throws Exception { - int anticipatedRequiredCapacity = ProtocolUtils.varIntBytes(msg.readableBytes()) - + msg.readableBytes(); + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, UncompressedPacket msg, + boolean preferDirect) throws Exception { + int anticipatedRequiredCapacity = ProtocolUtils.varIntBytes(msg.getPacketBuf().readableBytes()) + + msg.getPacketBuf().readableBytes(); return IS_JAVA_CIPHER ? ctx.alloc().heapBuffer(anticipatedRequiredCapacity) : ctx.alloc().directBuffer(anticipatedRequiredCapacity); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/CompressedPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/CompressedPacket.java new file mode 100644 index 0000000000..9cd154e136 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/CompressedPacket.java @@ -0,0 +1,97 @@ +/* + * 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.netty.data; + +import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible; +import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer; +import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; + +import com.velocitypowered.natives.compression.VelocityCompressor; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.util.zip.DataFormatException; + +/** + * Compressed identified packet. + */ +public class CompressedPacket extends IdentifiedPacket { + + private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB + private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB + + private static final int UNCOMPRESSED_CAP = + Boolean.getBoolean("velocity.increased-compression-cap") + ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; + + private final int uncompressedLength; + private final ByteBuf compressedBuf; + private final VelocityCompressor compressor; + + /** + * Constructs a binary compressed packet. + * + * @param packetId Packet ID. + * @param uncompressedLength Uncompressed packet length. + * @param compressedBuf Compressed buffer. + * @param compressor Compressor. + */ + public CompressedPacket(int packetId, int uncompressedLength, ByteBuf compressedBuf, + VelocityCompressor compressor) { + super(packetId); + this.uncompressedLength = uncompressedLength; + this.compressedBuf = compressedBuf; + this.compressor = compressor; + } + + /** + * Decompresses a buffer. + * + * @param allocator Buffer allocator. + * @return Target buffer. + * @throws DataFormatException Error occurred during decompression. + */ + public ByteBuf decompress(ByteBufAllocator allocator) throws DataFormatException { + checkFrame(this.uncompressedLength <= UNCOMPRESSED_CAP, + "Uncompressed size %s exceeds hard threshold of %s", this.uncompressedLength, + UNCOMPRESSED_CAP); + + ByteBuf compatibleIn = ensureCompatible(allocator, compressor, this.compressedBuf.duplicate()); + ByteBuf uncompressed = preferredBuffer(allocator, compressor, this.uncompressedLength); + try { + compressor.inflate(compatibleIn, uncompressed, this.uncompressedLength); + return uncompressed; + } catch (Exception e) { + uncompressed.release(); + throw e; + } finally { + compatibleIn.release(); + } + } + + public int getUncompressedLength() { + return this.uncompressedLength; + } + + public ByteBuf getCompressedBuf() { + return this.compressedBuf; + } + + public VelocityCompressor getCompressor() { + return this.compressor; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/IdentifiedPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/IdentifiedPacket.java new file mode 100644 index 0000000000..16930fbaa2 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/IdentifiedPacket.java @@ -0,0 +1,34 @@ +/* + * 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.netty.data; + +/** + * Abstract class for identified packets. + */ +public abstract class IdentifiedPacket { + + private final int packetId; + + public IdentifiedPacket(int packetId) { + this.packetId = packetId; + } + + public int getPacketId() { + return this.packetId; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/UncompressedPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/UncompressedPacket.java new file mode 100644 index 0000000000..359af2f6f5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/data/UncompressedPacket.java @@ -0,0 +1,88 @@ +/* + * 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.netty.data; + +import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible; +import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer; + +import com.velocitypowered.natives.compression.VelocityCompressor; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.util.zip.DataFormatException; + +/** + * Binary uncompressed identified packet. + */ +public class UncompressedPacket extends IdentifiedPacket { + + private final ByteBuf packetBuf; + + public UncompressedPacket(int packetId, ByteBuf packetBuf) { + super(packetId); + this.packetBuf = packetBuf; + } + + /** + * Allocates new buffer and compresses current packet buffer to it. + * + * @param compressor Compressor + * @param allocator Buffer allocator + * @return Allocated buffer + * @throws DataFormatException Error occurred during compression. + */ + public ByteBuf compress(VelocityCompressor compressor, ByteBufAllocator allocator) + throws DataFormatException { + ByteBuf compressed = preferredBuffer(allocator, compressor, 256); + try { + return compress(compressor, allocator, compressed); + } catch (DataFormatException e) { + compressed.release(); + throw e; + } + } + + /** + * Compresses packet buffer. + * + * @param compressor Compressor. + * @param allocator Buffer allocator. + * @param compressed Target buffer. + * @return Compressed buffer. + * @throws DataFormatException Error occurred during compression. + */ + public ByteBuf compress(VelocityCompressor compressor, ByteBufAllocator allocator, + ByteBuf compressed) throws DataFormatException { + ByteBuf compatibleIn = ensureCompatible(allocator, compressor, this.packetBuf.duplicate()); + + int originalWriterIndex = compressed.writerIndex(); + try { + compressor.deflate(compatibleIn, compressed); + } finally { + compatibleIn.release(); + } + int compressedLength = compressed.writerIndex() - originalWriterIndex; + if (compressedLength >= 1 << 21) { + throw new DataFormatException("Compressed packet is very large (over 2MiB)."); + } + return compressed; + } + + public ByteBuf getPacketBuf() { + return this.packetBuf; + } +} 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 837c46a252..b5747ff68a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -17,11 +17,12 @@ package com.velocitypowered.proxy.server; +import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER; import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; import static com.velocitypowered.proxy.network.Connections.HANDLER; import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; +import static com.velocitypowered.proxy.network.Connections.MINECRAFT_PRE_ENCODER; import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; import com.google.common.base.Preconditions; @@ -38,8 +39,9 @@ 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.MinecraftCompressAndIdDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftPreEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import io.netty.buffer.ByteBuf; @@ -116,8 +118,10 @@ protected void initChannel(Channel ch) throws Exception { ? server.getConfiguration().getReadTimeout() : pingOptions.getTimeout(), TimeUnit.MILLISECONDS)) .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(COMPRESSION_DECODER, new MinecraftCompressAndIdDecoder(server)) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) - .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); + .addLast(MINECRAFT_PRE_ENCODER, + new MinecraftPreEncoder(ProtocolUtils.Direction.SERVERBOUND)); ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); } diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 0f18208b55..c3158c2a57 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -102,6 +102,12 @@ compression-threshold = 256 # default level of 6. compression-level = -1 +# Packets with an uncompressed length below this value will always be decompressed and +# re-compressed depending on the compression threshold of the backend server and proxy. +# For larger packets, only the first 5 bytes will be decompressed to check whether the packet +# needs to be fully decompressed or it can be passed to the backend server/client as is. +decompression-threshold = 2048 + # How fast (in milliseconds) are clients allowed to connect after the last connection? By # default, this is three seconds. Disable this by setting this to 0. login-ratelimit = 3000