Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-16 21:10:30 +01:00
Merge branch 'dev/1.1.0' into dev/2.0.0
# Conflicts: # api/build.gradle # api/src/main/java/com/velocitypowered/api/proxy/connection/Player.java # build.gradle # proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java # proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java # proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java # proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java # proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressorAndLengthEncoder.java # proxy/src/test/java/com/velocitypowered/proxy/command/CommandManagerTests.java
Dieser Commit ist enthalten in:
Commit
a4e91793ca
1
.github/FUNDING.yml
vendored
Normale Datei
1
.github/FUNDING.yml
vendored
Normale Datei
@ -0,0 +1 @@
|
|||||||
|
github: astei
|
@ -76,13 +76,13 @@ javadoc {
|
|||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
options.charSet = 'UTF-8'
|
options.charSet = 'UTF-8'
|
||||||
options.source = '8'
|
options.source = '8'
|
||||||
// options.links(
|
options.links(
|
||||||
// 'https://www.slf4j.org/apidocs/',
|
'http://www.slf4j.org/apidocs/',
|
||||||
// 'https://guava.dev/releases/30.0-jre/api/docs/',
|
'https://google.github.io/guava/releases/30.0-jre/api/docs/',
|
||||||
// 'https://google.github.io/guice/api-docs/4.2/javadoc/',
|
'https://google.github.io/guice/api-docs/4.2/javadoc/',
|
||||||
// 'https://docs.oracle.com/javase/8/docs/api/',
|
'https://docs.oracle.com/javase/8/docs/api/',
|
||||||
// 'https://jd.adventure.kyori.net/api/4.0.0/'
|
'https://jd.adventure.kyori.net/api/4.7.0/'
|
||||||
// )
|
)
|
||||||
|
|
||||||
// Disable the crazy super-strict doclint tool in Java 8
|
// Disable the crazy super-strict doclint tool in Java 8
|
||||||
options.addStringOption('Xdoclint:none', '-quiet')
|
options.addStringOption('Xdoclint:none', '-quiet')
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
package com.velocitypowered.api.proxy.connection;
|
package com.velocitypowered.api.proxy.connection;
|
||||||
|
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEventImpl;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
||||||
import com.velocitypowered.api.proxy.player.ClientSettings;
|
import com.velocitypowered.api.proxy.player.ClientSettings;
|
||||||
@ -123,7 +124,7 @@ public interface Player extends CommandSource, Identified, InboundConnection,
|
|||||||
/**
|
/**
|
||||||
* Sends the specified resource pack from {@code url} to the user. If at all possible, send the
|
* Sends the specified resource pack from {@code url} to the user. If at all possible, send the
|
||||||
* resource pack using {@link #sendResourcePack(String, byte[])}. To monitor the status of the
|
* resource pack using {@link #sendResourcePack(String, byte[])}. To monitor the status of the
|
||||||
* sent resource pack, subscribe to {@link PlayerResourcePackStatusEventImpl}.
|
* sent resource pack, subscribe to {@link PlayerResourcePackStatusEvent}.
|
||||||
*
|
*
|
||||||
* @param url the URL for the resource pack
|
* @param url the URL for the resource pack
|
||||||
*/
|
*/
|
||||||
@ -132,10 +133,22 @@ public interface Player extends CommandSource, Identified, InboundConnection,
|
|||||||
/**
|
/**
|
||||||
* Sends the specified resource pack from {@code url} to the user, using the specified 20-byte
|
* Sends the specified resource pack from {@code url} to the user, using the specified 20-byte
|
||||||
* SHA-1 hash. To monitor the status of the sent resource pack, subscribe to
|
* SHA-1 hash. To monitor the status of the sent resource pack, subscribe to
|
||||||
* {@link PlayerResourcePackStatusEventImpl}.
|
* {@link PlayerResourcePackStatusEvent}.
|
||||||
*
|
*
|
||||||
* @param url the URL for the resource pack
|
* @param url the URL for the resource pack
|
||||||
* @param hash the SHA-1 hash value for the resource pack
|
* @param hash the SHA-1 hash value for the resource pack
|
||||||
*/
|
*/
|
||||||
void sendResourcePack(String url, byte[] hash);
|
void sendResourcePack(String url, byte[] hash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <strong>Note that this method does not send a plugin message to the server the player
|
||||||
|
* is connected to.</strong> You should only use this method if you are trying to communicate
|
||||||
|
* with a mod that is installed on the player's client. To send a plugin message to the server
|
||||||
|
* from the player, you should use the equivalent method on the instance returned by
|
||||||
|
* {@link #getCurrentServer()}.
|
||||||
|
*
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data);
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ import com.velocitypowered.proxy.network.packet.clientbound.ClientboundSetCompre
|
|||||||
import com.velocitypowered.proxy.network.pipeline.MinecraftCipherDecoder;
|
import com.velocitypowered.proxy.network.pipeline.MinecraftCipherDecoder;
|
||||||
import com.velocitypowered.proxy.network.pipeline.MinecraftCipherEncoder;
|
import com.velocitypowered.proxy.network.pipeline.MinecraftCipherEncoder;
|
||||||
import com.velocitypowered.proxy.network.pipeline.MinecraftCompressDecoder;
|
import com.velocitypowered.proxy.network.pipeline.MinecraftCompressDecoder;
|
||||||
import com.velocitypowered.proxy.network.pipeline.MinecraftCompressEncoder;
|
import com.velocitypowered.proxy.network.pipeline.MinecraftCompressorAndLengthEncoder;
|
||||||
import com.velocitypowered.proxy.network.pipeline.MinecraftDecoder;
|
import com.velocitypowered.proxy.network.pipeline.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.network.pipeline.MinecraftEncoder;
|
import com.velocitypowered.proxy.network.pipeline.MinecraftEncoder;
|
||||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||||
@ -408,8 +408,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
} else {
|
} else {
|
||||||
MinecraftCompressDecoder decoder = (MinecraftCompressDecoder) channel.pipeline()
|
MinecraftCompressDecoder decoder = (MinecraftCompressDecoder) channel.pipeline()
|
||||||
.get(COMPRESSION_DECODER);
|
.get(COMPRESSION_DECODER);
|
||||||
MinecraftCompressEncoder encoder = (MinecraftCompressEncoder) channel.pipeline()
|
MinecraftCompressorAndLengthEncoder encoder =
|
||||||
.get(COMPRESSION_ENCODER);
|
(MinecraftCompressorAndLengthEncoder) channel.pipeline().get(COMPRESSION_ENCODER);
|
||||||
if (decoder != null && encoder != null) {
|
if (decoder != null && encoder != null) {
|
||||||
decoder.setThreshold(threshold);
|
decoder.setThreshold(threshold);
|
||||||
encoder.setThreshold(threshold);
|
encoder.setThreshold(threshold);
|
||||||
@ -417,9 +417,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
int level = server.configuration().getCompressionLevel();
|
int level = server.configuration().getCompressionLevel();
|
||||||
VelocityCompressor compressor = Natives.compress.get().create(level);
|
VelocityCompressor compressor = Natives.compress.get().create(level);
|
||||||
|
|
||||||
encoder = new MinecraftCompressEncoder(threshold, compressor);
|
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
|
||||||
decoder = new MinecraftCompressDecoder(threshold, compressor);
|
decoder = new MinecraftCompressDecoder(threshold, compressor);
|
||||||
|
|
||||||
|
channel.pipeline().remove(FRAME_ENCODER);
|
||||||
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
||||||
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
||||||
}
|
}
|
||||||
|
@ -59,12 +59,16 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class);
|
||||||
private static final boolean BACKPRESSURE_LOG = Boolean
|
private static final boolean BACKPRESSURE_LOG = Boolean
|
||||||
.getBoolean("velocity.log-server-backpressure");
|
.getBoolean("velocity.log-server-backpressure");
|
||||||
|
private static final int MAXIMUM_PACKETS_TO_FLUSH = Integer
|
||||||
|
.getInteger("velocity.max-packets-per-flush", 8192);
|
||||||
|
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private final VelocityServerConnection serverConn;
|
private final VelocityServerConnection serverConn;
|
||||||
private final ClientPlaySessionHandler playerSessionHandler;
|
private final ClientPlaySessionHandler playerSessionHandler;
|
||||||
private final MinecraftConnection playerConnection;
|
private final MinecraftConnection playerConnection;
|
||||||
private final BungeeCordMessageResponder bungeecordMessageResponder;
|
private final BungeeCordMessageResponder bungeecordMessageResponder;
|
||||||
private boolean exceptionTriggered = false;
|
private boolean exceptionTriggered = false;
|
||||||
|
private int packetsFlushed;
|
||||||
|
|
||||||
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
@ -274,16 +278,25 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
((AbstractPluginMessagePacket<?>) packet).retain();
|
((AbstractPluginMessagePacket<?>) packet).retain();
|
||||||
}
|
}
|
||||||
playerConnection.delayedWrite(packet);
|
playerConnection.delayedWrite(packet);
|
||||||
|
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||||
|
playerConnection.flush();
|
||||||
|
packetsFlushed = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleUnknown(ByteBuf buf) {
|
public void handleUnknown(ByteBuf buf) {
|
||||||
playerConnection.delayedWrite(buf.retain());
|
playerConnection.delayedWrite(buf.retain());
|
||||||
|
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||||
|
playerConnection.flush();
|
||||||
|
packetsFlushed = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void readCompleted() {
|
public void readCompleted() {
|
||||||
playerConnection.flush();
|
playerConnection.flush();
|
||||||
|
packetsFlushed = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -59,7 +59,15 @@ public enum ProtocolUtils {
|
|||||||
|
|
||||||
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
|
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
|
||||||
private static final QuietDecoderException BAD_VARINT_CACHED =
|
private static final QuietDecoderException BAD_VARINT_CACHED =
|
||||||
new QuietDecoderException("Bad varint decoded");
|
new QuietDecoderException("Bad VarInt decoded");
|
||||||
|
private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (int i = 0; i <= 32; ++i) {
|
||||||
|
VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
|
||||||
|
}
|
||||||
|
VARINT_EXACT_BYTE_LENGTHS[32] = 1; // Special case for the number 0.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a Minecraft-style VarInt from the specified {@code buf}.
|
* Reads a Minecraft-style VarInt from the specified {@code buf}.
|
||||||
@ -69,7 +77,7 @@ public enum ProtocolUtils {
|
|||||||
public static int readVarInt(ByteBuf buf) {
|
public static int readVarInt(ByteBuf buf) {
|
||||||
int read = readVarIntSafely(buf);
|
int read = readVarIntSafely(buf);
|
||||||
if (read == Integer.MIN_VALUE) {
|
if (read == Integer.MIN_VALUE) {
|
||||||
throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad varint decoded")
|
throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
|
||||||
: BAD_VARINT_CACHED;
|
: BAD_VARINT_CACHED;
|
||||||
}
|
}
|
||||||
return read;
|
return read;
|
||||||
@ -95,23 +103,67 @@ public enum ProtocolUtils {
|
|||||||
return Integer.MIN_VALUE;
|
return Integer.MIN_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the exact byte size of {@code value} if it were encoded as a VarInt.
|
||||||
|
* @param value the value to encode
|
||||||
|
* @return the byte size of {@code value} if encoded as a VarInt
|
||||||
|
*/
|
||||||
|
public static int varIntBytes(int value) {
|
||||||
|
return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a Minecraft-style VarInt to the specified {@code buf}.
|
* Writes a Minecraft-style VarInt to the specified {@code buf}.
|
||||||
* @param buf the buffer to read from
|
* @param buf the buffer to read from
|
||||||
* @param value the integer to write
|
* @param value the integer to write
|
||||||
*/
|
*/
|
||||||
public static void writeVarInt(ByteBuf buf, int value) {
|
public static void writeVarInt(ByteBuf buf, int value) {
|
||||||
while (true) {
|
// Peel the one and two byte count cases explicitly as they are the most common VarInt sizes
|
||||||
if ((value & 0xFFFFFF80) == 0) {
|
// that the proxy will write, to improve inlining.
|
||||||
|
if ((value & (0xFFFFFFFF << 7)) == 0) {
|
||||||
buf.writeByte(value);
|
buf.writeByte(value);
|
||||||
return;
|
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
|
||||||
|
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
|
||||||
|
buf.writeShort(w);
|
||||||
|
} else {
|
||||||
|
writeVarIntFull(buf, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.writeByte(value & 0x7F | 0x80);
|
private static void writeVarIntFull(ByteBuf buf, int value) {
|
||||||
value >>>= 7;
|
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
||||||
|
if ((value & (0xFFFFFFFF << 7)) == 0) {
|
||||||
|
buf.writeByte(value);
|
||||||
|
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
|
||||||
|
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
|
||||||
|
buf.writeShort(w);
|
||||||
|
} else if ((value & (0xFFFFFFFF << 21)) == 0) {
|
||||||
|
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||||
|
buf.writeMedium(w);
|
||||||
|
} else if ((value & (0xFFFFFFFF << 28)) == 0) {
|
||||||
|
int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16)
|
||||||
|
| ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21);
|
||||||
|
buf.writeInt(w);
|
||||||
|
} else {
|
||||||
|
int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
|
||||||
|
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80);
|
||||||
|
buf.writeInt(w);
|
||||||
|
buf.writeByte(value >>> 28);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}.
|
||||||
|
* The upper 11 bits will be discarded.
|
||||||
|
* @param buf the buffer to read from
|
||||||
|
* @param value the integer to write
|
||||||
|
*/
|
||||||
|
public static void write21BitVarInt(ByteBuf buf, int value) {
|
||||||
|
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
||||||
|
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||||
|
buf.writeMedium(w);
|
||||||
|
}
|
||||||
|
|
||||||
public static String readString(ByteBuf buf) {
|
public static String readString(ByteBuf buf) {
|
||||||
return readString(buf, DEFAULT_MAX_STRING_SIZE);
|
return readString(buf, DEFAULT_MAX_STRING_SIZE);
|
||||||
}
|
}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 Velocity Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.velocitypowered.proxy.network.pipeline;
|
|
||||||
|
|
||||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
|
||||||
import com.velocitypowered.natives.util.MoreByteBufUtils;
|
|
||||||
import com.velocitypowered.proxy.network.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.handler.codec.MessageToByteEncoder;
|
|
||||||
|
|
||||||
public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
|
||||||
|
|
||||||
private int threshold;
|
|
||||||
private final VelocityCompressor compressor;
|
|
||||||
|
|
||||||
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
|
|
||||||
this.threshold = threshold;
|
|
||||||
this.compressor = compressor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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, 0);
|
|
||||||
out.writeBytes(msg);
|
|
||||||
} else {
|
|
||||||
ProtocolUtils.writeVarInt(out, uncompressed);
|
|
||||||
ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
|
|
||||||
try {
|
|
||||||
compressor.deflate(compatibleIn, out);
|
|
||||||
} finally {
|
|
||||||
compatibleIn.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
|
|
||||||
throws Exception {
|
|
||||||
// We allocate bytes to be compressed plus 1 byte. This covers two cases:
|
|
||||||
//
|
|
||||||
// - Compression
|
|
||||||
// According to https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103,
|
|
||||||
// if the data compresses well (and we do not have some pathological case) then the maximum
|
|
||||||
// size the compressed size will ever be is the input size minus one.
|
|
||||||
// - Uncompressed
|
|
||||||
// This is fairly obvious - we will then have one more than the uncompressed size.
|
|
||||||
int initialBufferSize = msg.readableBytes() + 1;
|
|
||||||
return MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
|
||||||
compressor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThreshold(int threshold) {
|
|
||||||
this.threshold = threshold;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.protocol.netty;
|
||||||
|
|
||||||
|
import static com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder.IS_JAVA_CIPHER;
|
||||||
|
|
||||||
|
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||||
|
import com.velocitypowered.natives.util.MoreByteBufUtils;
|
||||||
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.MessageToByteEncoder;
|
||||||
|
import java.util.zip.DataFormatException;
|
||||||
|
|
||||||
|
public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||||
|
|
||||||
|
private static final boolean MUST_USE_SAFE_AND_SLOW_COMPRESSION_HANDLING =
|
||||||
|
Boolean.getBoolean("velocity.increased-compression-cap");
|
||||||
|
|
||||||
|
private int threshold;
|
||||||
|
private final VelocityCompressor compressor;
|
||||||
|
|
||||||
|
public MinecraftCompressorAndLengthEncoder(int threshold, VelocityCompressor compressor) {
|
||||||
|
this.threshold = threshold;
|
||||||
|
this.compressor = compressor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
if (MUST_USE_SAFE_AND_SLOW_COMPRESSION_HANDLING) {
|
||||||
|
handleCompressedSafe(ctx, msg, out);
|
||||||
|
} else {
|
||||||
|
handleCompressedFast(ctx, msg, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCompressedFast(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out)
|
||||||
|
throws DataFormatException {
|
||||||
|
int uncompressed = msg.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. "
|
||||||
|
+ "Please restart Velocity with the JVM flag -Dvelocity.increased-compression-cap=true "
|
||||||
|
+ "to fix this issue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int writerIndex = out.writerIndex();
|
||||||
|
int packetLength = out.readableBytes() - 3;
|
||||||
|
out.writerIndex(0);
|
||||||
|
ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length
|
||||||
|
out.writerIndex(writerIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCompressedSafe(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out)
|
||||||
|
throws DataFormatException {
|
||||||
|
int uncompressed = msg.readableBytes();
|
||||||
|
ByteBuf tmpBuf = MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, uncompressed - 1);
|
||||||
|
try {
|
||||||
|
ProtocolUtils.writeVarInt(tmpBuf, uncompressed);
|
||||||
|
ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
|
||||||
|
try {
|
||||||
|
compressor.deflate(compatibleIn, tmpBuf);
|
||||||
|
} finally {
|
||||||
|
compatibleIn.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolUtils.writeVarInt(out, tmpBuf.readableBytes());
|
||||||
|
out.writeBytes(tmpBuf);
|
||||||
|
} finally {
|
||||||
|
tmpBuf.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
|
||||||
|
throws Exception {
|
||||||
|
int uncompressed = msg.readableBytes();
|
||||||
|
if (uncompressed < threshold) {
|
||||||
|
int finalBufferSize = uncompressed + 1;
|
||||||
|
finalBufferSize += ProtocolUtils.varIntBytes(finalBufferSize);
|
||||||
|
return IS_JAVA_CIPHER
|
||||||
|
? ctx.alloc().heapBuffer(finalBufferSize)
|
||||||
|
: ctx.alloc().directBuffer(finalBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (maximum data length after compression) + packet length varint + uncompressed data varint
|
||||||
|
int initialBufferSize = (uncompressed - 1) + 3 + ProtocolUtils.varIntBytes(uncompressed);
|
||||||
|
return MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
compressor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThreshold(int threshold) {
|
||||||
|
this.threshold = threshold;
|
||||||
|
}
|
||||||
|
}
|
@ -63,7 +63,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
|
private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
|
||||||
if (!ctx.channel().isActive()) {
|
if (!ctx.channel().isActive() || !buf.isReadable()) {
|
||||||
buf.release();
|
buf.release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ import io.netty.handler.codec.MessageToByteEncoder;
|
|||||||
public class MinecraftVarintLengthEncoder extends MessageToByteEncoder<ByteBuf> {
|
public class MinecraftVarintLengthEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||||
|
|
||||||
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
|
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
|
||||||
private static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY;
|
public static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY;
|
||||||
|
|
||||||
private MinecraftVarintLengthEncoder() {
|
private MinecraftVarintLengthEncoder() {
|
||||||
}
|
}
|
||||||
@ -43,7 +43,8 @@ public class MinecraftVarintLengthEncoder extends MessageToByteEncoder<ByteBuf>
|
|||||||
@Override
|
@Override
|
||||||
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
|
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
int anticipatedRequiredCapacity = 5 + msg.readableBytes();
|
int anticipatedRequiredCapacity = ProtocolUtils.varIntBytes(msg.readableBytes())
|
||||||
|
+ msg.readableBytes();
|
||||||
return IS_JAVA_CIPHER
|
return IS_JAVA_CIPHER
|
||||||
? ctx.alloc().heapBuffer(anticipatedRequiredCapacity)
|
? ctx.alloc().heapBuffer(anticipatedRequiredCapacity)
|
||||||
: ctx.alloc().directBuffer(anticipatedRequiredCapacity);
|
: ctx.alloc().directBuffer(anticipatedRequiredCapacity);
|
||||||
|
@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.protocol;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.ByteBufUtil;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class ProtocolUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void negativeVarIntBytes() {
|
||||||
|
assertEquals(5, ProtocolUtils.varIntBytes(-1));
|
||||||
|
assertEquals(5, ProtocolUtils.varIntBytes(Integer.MIN_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zeroVarIntBytes() {
|
||||||
|
assertEquals(1, ProtocolUtils.varIntBytes(0));
|
||||||
|
assertEquals(1, ProtocolUtils.varIntBytes(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ensureConsistencyAcrossNumberBits() {
|
||||||
|
for (int i = 0; i <= 31; i++) {
|
||||||
|
int number = (1 << i) - 1;
|
||||||
|
assertEquals(conventionalWrittenBytes(number), ProtocolUtils.varIntBytes(number),
|
||||||
|
"mismatch with " + i + "-bit number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPositiveOld() {
|
||||||
|
ByteBuf buf = Unpooled.buffer(5);
|
||||||
|
for (int i = 0; i >= 0; i += 127) {
|
||||||
|
writeReadTestOld(buf, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNegativeOld() {
|
||||||
|
ByteBuf buf = Unpooled.buffer(5);
|
||||||
|
for (int i = 0; i <= 0; i -= 127) {
|
||||||
|
writeReadTestOld(buf, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeReadTestOld(ByteBuf buf, int test) {
|
||||||
|
buf.clear();
|
||||||
|
writeVarIntOld(buf, test);
|
||||||
|
assertEquals(test, ProtocolUtils.readVarIntSafely(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test3Bytes() {
|
||||||
|
ByteBuf buf = Unpooled.buffer(5);
|
||||||
|
for (int i = 0; i < 2097152; i += 31) {
|
||||||
|
writeReadTest3Bytes(buf, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeReadTest3Bytes(ByteBuf buf, int test) {
|
||||||
|
buf.clear();
|
||||||
|
ProtocolUtils.write21BitVarInt(buf, test);
|
||||||
|
assertEquals(test, ProtocolUtils.readVarInt(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBytesWrittenAtBitBoundaries() {
|
||||||
|
ByteBuf varintNew = Unpooled.buffer(5);
|
||||||
|
ByteBuf varintOld = Unpooled.buffer(5);
|
||||||
|
|
||||||
|
long bytesNew = 0;
|
||||||
|
long bytesOld = 0;
|
||||||
|
for (int bit = 0; bit <= 31; bit++) {
|
||||||
|
int i = (1 << bit) - 1;
|
||||||
|
|
||||||
|
writeVarIntOld(varintOld, i);
|
||||||
|
ProtocolUtils.writeVarInt(varintNew, i);
|
||||||
|
assertArrayEquals(ByteBufUtil.getBytes(varintOld), ByteBufUtil.getBytes(varintNew),
|
||||||
|
"Encoding of " + i + " was invalid");
|
||||||
|
|
||||||
|
assertEquals(i, oldReadVarIntSafely(varintNew));
|
||||||
|
assertEquals(i, ProtocolUtils.readVarIntSafely(varintOld));
|
||||||
|
|
||||||
|
varintNew.clear();
|
||||||
|
varintOld.clear();
|
||||||
|
}
|
||||||
|
assertEquals(bytesNew, bytesOld, "byte sizes differ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBytesWritten() {
|
||||||
|
ByteBuf varintNew = Unpooled.buffer(5);
|
||||||
|
ByteBuf varintOld = Unpooled.buffer(5);
|
||||||
|
|
||||||
|
long bytesNew = 0;
|
||||||
|
long bytesOld = 0;
|
||||||
|
for (int i = 0; i <= 1_000_000; i++) {
|
||||||
|
ProtocolUtils.writeVarInt(varintNew, i);
|
||||||
|
writeVarIntOld(varintOld, i);
|
||||||
|
bytesNew += varintNew.readableBytes();
|
||||||
|
bytesOld += varintOld.readableBytes();
|
||||||
|
varintNew.clear();
|
||||||
|
varintOld.clear();
|
||||||
|
}
|
||||||
|
assertEquals(bytesNew, bytesOld, "byte sizes differ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int oldReadVarIntSafely(ByteBuf buf) {
|
||||||
|
int i = 0;
|
||||||
|
int maxRead = Math.min(5, buf.readableBytes());
|
||||||
|
for (int j = 0; j < maxRead; j++) {
|
||||||
|
int k = buf.readByte();
|
||||||
|
i |= (k & 0x7F) << j * 7;
|
||||||
|
if ((k & 0x80) != 128) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeVarIntOld(ByteBuf buf, int value) {
|
||||||
|
while (true) {
|
||||||
|
if ((value & 0xFFFFFF80) == 0) {
|
||||||
|
buf.writeByte(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.writeByte(value & 0x7F | 0x80);
|
||||||
|
value >>>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int conventionalWrittenBytes(int value) {
|
||||||
|
int wouldBeWritten = 0;
|
||||||
|
while (true) {
|
||||||
|
if ((value & ~0x7FL) == 0) {
|
||||||
|
wouldBeWritten++;
|
||||||
|
return wouldBeWritten;
|
||||||
|
} else {
|
||||||
|
wouldBeWritten++;
|
||||||
|
value >>>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren