diff --git a/libs.versions.toml b/libs.versions.toml index 550309ec3d..d56236b578 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -7,6 +7,7 @@ protocol-support = "3d24efeda6" paper = "1.20.6-R0.1-SNAPSHOT" bungeecord = "1.20-R0.1-SNAPSHOT" velocity = "3.1.0" +velocity-native = "3.3.0-SNAPSHOT" run-paper = "2.3.0" [libraries] @@ -23,6 +24,7 @@ protocol-support = { group = "com.github.ProtocolSupport", name = "ProtocolSuppo paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } bungeecord = { group = "net.md-5", name = "bungeecord-api", version.ref = "bungeecord" } velocity = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity" } +velocity-native = { group = "com.velocitypowered", name = "velocity-native", version.ref = "velocity-native" } [bundles] adventure = [ "adventure-api", "adventure-nbt" ] diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 4102f5bd52..9e21f7d379 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -13,6 +13,7 @@ repositories { dependencies { compileOnly(libs.netty) compileOnly(libs.velocity) + compileOnly(libs.velocity.native) annotationProcessor(libs.velocity) shadow(project(":api", "shadow")) shadow(project(":netty-common")) diff --git a/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsDecoder.java b/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsDecoder.java index 3e978cf98b..85f03a1fda 100644 --- a/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsDecoder.java +++ b/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsDecoder.java @@ -1,6 +1,6 @@ /* * This file is part of packetevents - https://github.com/retrooper/packetevents - * Copyright (C) 2022 retrooper and contributors + * Copyright (C) 2024 retrooper and 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 @@ -21,7 +21,6 @@ import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketReceiveEvent; import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper; -import com.github.retrooper.packetevents.netty.channel.ChannelHelper; import com.github.retrooper.packetevents.protocol.player.User; import com.github.retrooper.packetevents.util.EnumUtil; import com.github.retrooper.packetevents.util.EventCreationUtil; @@ -39,10 +38,12 @@ @ChannelHandler.Sharable public class PacketEventsDecoder extends MessageToMessageDecoder { - private static Enum VELOCITY_CONNECTION_EVENT_CONSTANT; + private static Enum VELOCITY_CONNECTION_EVENT_COMPRESSION_ENABLED; + private static Enum VELOCITY_CONNECTION_EVENT_COMPRESSION_DISABLED; public User user; public Player player; public boolean handledCompression; + public PacketEventsDecoder(User user) { this.user = user; } @@ -83,19 +84,43 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List o @Override public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception { - if (VELOCITY_CONNECTION_EVENT_CONSTANT == null) { + if (VELOCITY_CONNECTION_EVENT_COMPRESSION_ENABLED == null) { Class> clazz = (Class>) Reflection.getClassByNameWithoutException("com.velocitypowered.proxy.protocol.VelocityConnectionEvent"); - VELOCITY_CONNECTION_EVENT_CONSTANT = EnumUtil.valueOf(clazz, "COMPRESSION_ENABLED"); + VELOCITY_CONNECTION_EVENT_COMPRESSION_ENABLED = EnumUtil.valueOf(clazz, "COMPRESSION_ENABLED"); + VELOCITY_CONNECTION_EVENT_COMPRESSION_DISABLED = EnumUtil.valueOf(clazz, "COMPRESSION_DISABLED"); } //We can use == as it is an enum constant - if (event == VELOCITY_CONNECTION_EVENT_CONSTANT && !handledCompression) { - ChannelPipeline pipe = ctx.pipeline(); - PacketEventsEncoder encoder = (PacketEventsEncoder) pipe.remove(PacketEvents.ENCODER_NAME); - pipe.addBefore("minecraft-encoder", PacketEvents.ENCODER_NAME, encoder); - PacketEventsDecoder decoder = (PacketEventsDecoder) pipe.remove(PacketEvents.DECODER_NAME); - pipe.addBefore("minecraft-decoder", PacketEvents.DECODER_NAME, decoder); - //System.out.println("Pipe: " + ChannelHelper.pipelineHandlerNamesAsString(ctx.channel())); - handledCompression = true; + if (event == VELOCITY_CONNECTION_EVENT_COMPRESSION_ENABLED || event == VELOCITY_CONNECTION_EVENT_COMPRESSION_DISABLED) { + ChannelPipeline pipeline = ctx.pipeline(); + + // Check if FastPrepareAPI is injected + ChannelHandlerContext context = pipeline.context("fastprepare-encoder"); + if (context != null) { + // Use uncompressed packets instead of compressed ones. + context.handler().getClass().getDeclaredMethod("setShouldSendUncompressed", boolean.class).invoke(context.handler(), true); + } + + PacketEventsEncoder encoder = (PacketEventsEncoder) pipeline.get(PacketEvents.ENCODER_NAME); + encoder.enableCompression = event == VELOCITY_CONNECTION_EVENT_COMPRESSION_ENABLED; + + // Relocate handlers if FastPrepareAPI was injected or dejected + boolean wasInjected = encoder.fastPrepareApiInjected; + encoder.fastPrepareApiInjected = context != null; + if (wasInjected != encoder.fastPrepareApiInjected) { + handledCompression = false; + } + + if (!handledCompression) { + encoder.ignoreRemoval = true; + encoder = (PacketEventsEncoder) pipeline.remove(PacketEvents.ENCODER_NAME); + pipeline.addBefore("minecraft-encoder", PacketEvents.ENCODER_NAME, encoder); + encoder.ignoreRemoval = false; + + PacketEventsDecoder decoder = (PacketEventsDecoder) pipeline.remove(PacketEvents.DECODER_NAME); + pipeline.addBefore("minecraft-decoder", PacketEvents.DECODER_NAME, decoder); + //System.out.println("Pipe: " + ChannelHelper.pipelineHandlerNamesAsString(ctx.channel())); + handledCompression = true; + } } super.userEventTriggered(ctx, event); } diff --git a/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsEncoder.java b/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsEncoder.java index ed4d2c3f71..0b4c72aac7 100644 --- a/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsEncoder.java +++ b/velocity/src/main/java/io/github/retrooper/packetevents/handlers/PacketEventsEncoder.java @@ -1,6 +1,6 @@ /* * This file is part of packetevents - https://github.com/retrooper/packetevents - * Copyright (C) 2022 retrooper and contributors + * Copyright (C) 2024 retrooper and 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 @@ -21,9 +21,15 @@ import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketSendEvent; import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper; +import com.github.retrooper.packetevents.protocol.ConnectionState; import com.github.retrooper.packetevents.protocol.player.User; import com.github.retrooper.packetevents.util.EventCreationUtil; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.config.ProxyConfig; +import com.velocitypowered.natives.compression.VelocityCompressor; +import com.velocitypowered.natives.util.MoreByteBufUtils; +import com.velocitypowered.natives.util.Natives; +import io.github.retrooper.packetevents.injector.VelocityPipelineInjector; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; @@ -31,8 +37,15 @@ @ChannelHandler.Sharable public class PacketEventsEncoder extends MessageToByteEncoder { + private static Boolean FASTMOTD_PRESENT; public Player player; public User user; + public int compressionThreshold; + public VelocityCompressor compressor; + public boolean fastPrepareApiInjected; + public boolean enableCompression; + public boolean enableCompressionClientside; + public boolean ignoreRemoval; public PacketEventsEncoder(User user) { this.user = user; @@ -40,6 +53,23 @@ public PacketEventsEncoder(User user) { public void read(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { int firstReaderIndex = buffer.readerIndex(); + + boolean rewriteFrameSize = false; + if (this.user.getEncoderState() == ConnectionState.STATUS && buffer.isReadable()) { + if (FASTMOTD_PRESENT == null) { + VelocityPipelineInjector injector = (VelocityPipelineInjector) PacketEvents.getAPI().getInjector(); + FASTMOTD_PRESENT = injector.getServer().getPluginManager().getPlugin("fastmotd").isPresent(); + } + + if (FASTMOTD_PRESENT) { + int frameSize = ByteBufHelper.readVarInt(buffer); + rewriteFrameSize = frameSize > 0 && frameSize == buffer.readableBytes(); + if (!rewriteFrameSize) { + buffer.readerIndex(firstReaderIndex); + } + } + } + PacketSendEvent packetSendEvent = EventCreationUtil.createSendEvent(ctx.channel(), user, player, buffer, false); int readerIndex = buffer.readerIndex(); @@ -47,8 +77,17 @@ public void read(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { if (!packetSendEvent.isCancelled()) { if (packetSendEvent.getLastUsedWrapper() != null) { ByteBufHelper.clear(packetSendEvent.getByteBuf()); + if (rewriteFrameSize) { + buffer.writeMedium(0); + } + packetSendEvent.getLastUsedWrapper().writeVarInt(packetSendEvent.getPacketId()); packetSendEvent.getLastUsedWrapper().write(); + + if (rewriteFrameSize) { + int frameSize = buffer.readableBytes() - 3; + buffer.setMedium(0, (frameSize & 0x7F | 0x80) << 16 | ((frameSize >>> 7) & 0x7F | 0x80) << 8 | (frameSize >>> 14)); + } } buffer.readerIndex(firstReaderIndex); } else { @@ -61,10 +100,76 @@ public void read(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { } } + private VelocityCompressor findCompressor() { + if (this.compressor != null) { + return this.compressor; + } + + VelocityPipelineInjector injector = (VelocityPipelineInjector) PacketEvents.getAPI().getInjector(); + ProxyConfig config = injector.getServer().getConfiguration(); + + this.compressionThreshold = config.getCompressionThreshold(); + if (this.compressionThreshold != -1) { + this.compressor = Natives.compress.get().create(config.getCompressionLevel()); + } + + return this.compressor; + } + @Override protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { if (!msg.isReadable()) return; + // FastPrepareAPI is injected, completly re-encode packets. + if (this.fastPrepareApiInjected) { + // As FastPrepareAPI can send multiple packets at a single 'encode', we should process them all + while (msg.isReadable()) { + int size = ByteBufHelper.readVarInt(msg); + ByteBuf transformed = ctx.alloc().buffer(size).writeBytes(msg, size); + try { + read(ctx, transformed); + if (!transformed.isReadable()) { + continue; + } + + // Re-encode transformed packet + if (this.enableCompression && this.enableCompressionClientside) { + VelocityCompressor velocityCompressor = this.findCompressor(); + + // Write frame size + uncompressed length + uncompressed or compressed packet data + int uncompressed = transformed.readableBytes(); + if (uncompressed < this.compressionThreshold) { + ByteBufHelper.writeVarInt(out, uncompressed + 1); + ByteBufHelper.writeVarInt(out, 0); + out.writeBytes(transformed); + } else { + int frameIndex = out.writerIndex(); + out.writeMedium(0); + + ByteBufHelper.writeVarInt(out, uncompressed); + ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), velocityCompressor, transformed); + + try { + velocityCompressor.deflate(compatible, out); + } finally { + compatible.release(); + } + + int frameSize = (out.writerIndex() - frameIndex) - 3; + out.setMedium(frameIndex, (frameSize & 0x7F | 0x80) << 16 | ((frameSize >>> 7) & 0x7F | 0x80) << 8 | (frameSize >>> 14)); + } + } else { + // Write frame size + packet data + ByteBufHelper.writeVarInt(out, transformed.readableBytes()); + out.writeBytes(transformed); + } + } finally { + transformed.release(); + } + } + return; + } + ByteBuf transformed = ctx.alloc().buffer().writeBytes(msg); try { read(ctx, transformed); @@ -74,6 +179,14 @@ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throw } } + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + if (this.compressor != null && !this.ignoreRemoval) { + this.compressor.close(); + this.compressor = null; + } + } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); diff --git a/velocity/src/main/java/io/github/retrooper/packetevents/injector/VelocityPipelineInjector.java b/velocity/src/main/java/io/github/retrooper/packetevents/injector/VelocityPipelineInjector.java index 87d01964ba..27b84a3d50 100644 --- a/velocity/src/main/java/io/github/retrooper/packetevents/injector/VelocityPipelineInjector.java +++ b/velocity/src/main/java/io/github/retrooper/packetevents/injector/VelocityPipelineInjector.java @@ -1,6 +1,6 @@ /* * This file is part of packetevents - https://github.com/retrooper/packetevents - * Copyright (C) 2022 retrooper and contributors + * Copyright (C) 2024 retrooper and 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 @@ -43,6 +43,10 @@ public VelocityPipelineInjector(ProxyServer server) { this.server = server; } + public ProxyServer getServer() { + return this.server; + } + @Override public boolean isServerBound() { return true; diff --git a/velocity/src/main/java/io/github/retrooper/packetevents/manager/InternalVelocityPacketListener.java b/velocity/src/main/java/io/github/retrooper/packetevents/manager/InternalVelocityPacketListener.java new file mode 100644 index 0000000000..efaaedc311 --- /dev/null +++ b/velocity/src/main/java/io/github/retrooper/packetevents/manager/InternalVelocityPacketListener.java @@ -0,0 +1,42 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and 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 io.github.retrooper.packetevents.manager; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.manager.InternalPacketListener; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.login.server.WrapperLoginServerSetCompression; +import io.github.retrooper.packetevents.handlers.PacketEventsEncoder; +import io.netty.channel.Channel; + +public class InternalVelocityPacketListener extends InternalPacketListener { + + @Override + public void onPacketSend(PacketSendEvent event) { + if (event.getPacketType() == PacketType.Login.Server.SET_COMPRESSION) { + WrapperLoginServerSetCompression compression = new WrapperLoginServerSetCompression(event); + Channel channel = (Channel) event.getUser().getChannel(); + PacketEventsEncoder encoder = (PacketEventsEncoder) channel.pipeline().get(PacketEvents.ENCODER_NAME); + encoder.enableCompressionClientside = compression.getThreshold() >= 0; + } + + super.onPacketSend(event); + } +} diff --git a/velocity/src/main/java/io/github/retrooper/packetevents/velocity/factory/VelocityPacketEventsBuilder.java b/velocity/src/main/java/io/github/retrooper/packetevents/velocity/factory/VelocityPacketEventsBuilder.java index d89a655419..b8a9fdf81d 100644 --- a/velocity/src/main/java/io/github/retrooper/packetevents/velocity/factory/VelocityPacketEventsBuilder.java +++ b/velocity/src/main/java/io/github/retrooper/packetevents/velocity/factory/VelocityPacketEventsBuilder.java @@ -1,6 +1,6 @@ /* * This file is part of packetevents - https://github.com/retrooper/packetevents - * Copyright (C) 2022 retrooper and contributors + * Copyright (C) 2024 retrooper and 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 @@ -22,7 +22,6 @@ import com.github.retrooper.packetevents.PacketEventsAPI; import com.github.retrooper.packetevents.event.UserLoginEvent; import com.github.retrooper.packetevents.injector.ChannelInjector; -import com.github.retrooper.packetevents.manager.InternalPacketListener; import com.github.retrooper.packetevents.manager.player.PlayerManager; import com.github.retrooper.packetevents.manager.protocol.ProtocolManager; import com.github.retrooper.packetevents.manager.server.ServerManager; @@ -43,6 +42,7 @@ import io.github.retrooper.packetevents.impl.netty.manager.protocol.ProtocolManagerAbstract; import io.github.retrooper.packetevents.impl.netty.manager.server.ServerManagerAbstract; import io.github.retrooper.packetevents.injector.VelocityPipelineInjector; +import io.github.retrooper.packetevents.manager.InternalVelocityPacketListener; import io.github.retrooper.packetevents.manager.PlayerManagerImpl; import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.Nullable; @@ -144,7 +144,7 @@ public void load() { // Register internal packet listener (should be the first listener) // This listener doesn't do any modifications to the packets, just reads data - getEventManager().registerListener(new InternalPacketListener()); + getEventManager().registerListener(new InternalVelocityPacketListener()); } }