diff --git a/.gitignore b/.gitignore index de5e97e42..74b81462f 100644 --- a/.gitignore +++ b/.gitignore @@ -87,5 +87,5 @@ run/ plugins/ ### Natives stuff ### -natives/mbedtls -natives/zlib-ng \ No newline at end of file +native/mbedtls +native/zlib-ng diff --git a/.travis.yml b/.travis.yml index 3d0c36340..ff0d0f19d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,5 @@ cache: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ jdk: - - openjdk8 \ No newline at end of file + - openjdk8 + - openjdk11 \ No newline at end of file diff --git a/api/build.gradle b/api/build.gradle index a9e466865..edf3dc198 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -73,6 +73,9 @@ javadoc { // Disable the crazy super-strict doclint tool in Java 8 options.addStringOption('Xdoclint:none', '-quiet') + + // Mark sources as Java 8 source compatible + options.source = '8' } publishing { diff --git a/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java index 87ae8c3f0..e08e1d741 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java @@ -147,10 +147,12 @@ public final class KickedFromServerEvent implements */ public static final class RedirectPlayer implements ServerKickResult { + private final Component message; private final RegisteredServer server; - private RedirectPlayer(RegisteredServer server) { + private RedirectPlayer(RegisteredServer server, @Nullable Component message) { this.server = Preconditions.checkNotNull(server, "server"); + this.message = message; } @Override @@ -162,14 +164,23 @@ public final class KickedFromServerEvent implements return server; } + @Nullable + public Component getMessage() { + return message; + } + /** * Creates a new redirect result to forward the player to the specified {@code server}. * * @param server the server to send the player to * @return the redirect result */ - public static RedirectPlayer create(RegisteredServer server) { - return new RedirectPlayer(server); + public static RedirectPlayer create(RegisteredServer server, @Nullable Component message) { + return new RedirectPlayer(server, message); + } + + public static ServerKickResult create(RegisteredServer server) { + return create(server, null); } } diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChooseInitialServerEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChooseInitialServerEvent.java new file mode 100644 index 000000000..3ff289363 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChooseInitialServerEvent.java @@ -0,0 +1,51 @@ +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Fired when a player has finished connecting to the proxy and we need to choose the first server + * to connect to. + */ +public class PlayerChooseInitialServerEvent { + + private final Player player; + private @Nullable RegisteredServer initialServer; + + /** + * Constructs a PlayerChooseInitialServerEvent. + * @param player the player that was connected + * @param initialServer the initial server selected, may be {@code null} + */ + public PlayerChooseInitialServerEvent(Player player, @Nullable RegisteredServer initialServer) { + this.player = Preconditions.checkNotNull(player, "player"); + this.initialServer = initialServer; + } + + public Player getPlayer() { + return player; + } + + public Optional getInitialServer() { + return Optional.ofNullable(initialServer); + } + + /** + * Sets the new initial server. + * @param server the initial server the player should connect to + */ + public void setInitialServer(RegisteredServer server) { + this.initialServer = server; + } + + @Override + public String toString() { + return "PlayerChooseInitialServerEvent{" + + "player=" + player + + ", initialServer=" + initialServer + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java index 2cd5f3eaf..0609798c1 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java @@ -3,6 +3,8 @@ package com.velocitypowered.api.event.player; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.server.RegisteredServer; +import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** * This event is fired once the player has successfully connected to the target server and the @@ -12,10 +14,24 @@ public final class ServerConnectedEvent { private final Player player; private final RegisteredServer server; + private final @Nullable RegisteredServer previousServer; - public ServerConnectedEvent(Player player, RegisteredServer server) { + /** + * Constructs a ServerConnectedEvent. + * @param player the player that was connected + * @param server the server the player was connected to + * @param previousServer the server the player was previously connected to, null if none + */ + public ServerConnectedEvent(Player player, RegisteredServer server, + @Nullable RegisteredServer previousServer) { this.player = Preconditions.checkNotNull(player, "player"); this.server = Preconditions.checkNotNull(server, "server"); + this.previousServer = previousServer; + } + + @Deprecated + public ServerConnectedEvent(Player player, RegisteredServer server) { + this(player, server, null); } public Player getPlayer() { @@ -26,11 +42,16 @@ public final class ServerConnectedEvent { return server; } + public Optional getPreviousServer() { + return Optional.ofNullable(previousServer); + } + @Override public String toString() { return "ServerConnectedEvent{" + "player=" + player + ", server=" + server + + ", previousServer=" + previousServer + '}'; } } diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 93b1a3720..5c7d849a8 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -33,7 +33,8 @@ public enum ProtocolVersion { MINECRAFT_1_14_1(480, "1.14.1"), MINECRAFT_1_14_2(485, "1.14.2"), MINECRAFT_1_14_3(490, "1.14.3"), - MINECRAFT_1_14_4(498, "1.14.4"); + MINECRAFT_1_14_4(498, "1.14.4"), + MINECRAFT_1_15(566, "1.15-pre2"); private final int protocol; private final String name; diff --git a/api/src/main/java/com/velocitypowered/api/util/FastUuidSansHyphens.java b/api/src/main/java/com/velocitypowered/api/util/FastUuidSansHyphens.java new file mode 100644 index 000000000..3747d5034 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/FastUuidSansHyphens.java @@ -0,0 +1,195 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 Jon Chambers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.velocitypowered.api.util; + +import java.util.UUID; + +/** + * This is a modified FastUUID implementation. The primary difference is that it does not dash its + * UUIDs. As the native Java 9+ UUID.toString() implementation dashes its UUIDs, we use the FastUUID + * internal method, which ought to be faster than a String.replace(). + */ +class FastUuidSansHyphens { + + private static final int MOJANG_BROKEN_UUID_LENGTH = 32; + + private static final char[] HEX_DIGITS = + new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + private static final long[] HEX_VALUES = new long[128]; + + static { + for (int i = 0; i < HEX_VALUES.length; i++) { + HEX_VALUES[i] = -1; + } + + HEX_VALUES['0'] = 0x0; + HEX_VALUES['1'] = 0x1; + HEX_VALUES['2'] = 0x2; + HEX_VALUES['3'] = 0x3; + HEX_VALUES['4'] = 0x4; + HEX_VALUES['5'] = 0x5; + HEX_VALUES['6'] = 0x6; + HEX_VALUES['7'] = 0x7; + HEX_VALUES['8'] = 0x8; + HEX_VALUES['9'] = 0x9; + + HEX_VALUES['a'] = 0xa; + HEX_VALUES['b'] = 0xb; + HEX_VALUES['c'] = 0xc; + HEX_VALUES['d'] = 0xd; + HEX_VALUES['e'] = 0xe; + HEX_VALUES['f'] = 0xf; + + HEX_VALUES['A'] = 0xa; + HEX_VALUES['B'] = 0xb; + HEX_VALUES['C'] = 0xc; + HEX_VALUES['D'] = 0xd; + HEX_VALUES['E'] = 0xe; + HEX_VALUES['F'] = 0xf; + } + + private FastUuidSansHyphens() { + // A private constructor prevents callers from accidentally instantiating FastUUID instances + } + + /** + * Parses a UUID from the given character sequence. The character sequence must represent a + * Mojang UUID. + * + * @param uuidSequence the character sequence from which to parse a UUID + * + * @return the UUID represented by the given character sequence + * + * @throws IllegalArgumentException if the given character sequence does not conform to the string + * representation of a Mojang UUID. + */ + static UUID parseUuid(final CharSequence uuidSequence) { + if (uuidSequence.length() != MOJANG_BROKEN_UUID_LENGTH) { + throw new IllegalArgumentException("Illegal UUID string: " + uuidSequence); + } + + long mostSignificantBits = getHexValueForChar(uuidSequence.charAt(0)) << 60; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(1)) << 56; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(2)) << 52; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(3)) << 48; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(4)) << 44; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(5)) << 40; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(6)) << 36; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(7)) << 32; + + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(8)) << 28; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(9)) << 24; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(10)) << 20; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(11)) << 16; + + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(12)) << 12; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(13)) << 8; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(14)) << 4; + mostSignificantBits |= getHexValueForChar(uuidSequence.charAt(15)); + + long leastSignificantBits = getHexValueForChar(uuidSequence.charAt(16)) << 60; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(17)) << 56; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(18)) << 52; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(19)) << 48; + + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(20)) << 44; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(21)) << 40; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(22)) << 36; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(23)) << 32; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(24)) << 28; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(25)) << 24; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(26)) << 20; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(27)) << 16; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(28)) << 12; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(29)) << 8; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(30)) << 4; + leastSignificantBits |= getHexValueForChar(uuidSequence.charAt(31)); + + return new UUID(mostSignificantBits, leastSignificantBits); + } + + /** + * Returns a string representation of the given UUID. The returned string is formatted as a + * Mojang-style UUID. + * + * @param uuid the UUID to represent as a string + * + * @return a string representation of the given UUID + */ + public static String toString(final UUID uuid) { + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + final char[] uuidChars = new char[MOJANG_BROKEN_UUID_LENGTH]; + + uuidChars[0] = HEX_DIGITS[(int) ((mostSignificantBits & 0xf000000000000000L) >>> 60)]; + uuidChars[1] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0f00000000000000L) >>> 56)]; + uuidChars[2] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00f0000000000000L) >>> 52)]; + uuidChars[3] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000f000000000000L) >>> 48)]; + uuidChars[4] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000f00000000000L) >>> 44)]; + uuidChars[5] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000f0000000000L) >>> 40)]; + uuidChars[6] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000f000000000L) >>> 36)]; + uuidChars[7] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000f00000000L) >>> 32)]; + uuidChars[8] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000f0000000L) >>> 28)]; + uuidChars[9] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000f000000L) >>> 24)]; + uuidChars[10] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000f00000L) >>> 20)]; + uuidChars[11] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000f0000L) >>> 16)]; + uuidChars[12] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000000f000L) >>> 12)]; + uuidChars[13] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000000f00L) >>> 8)]; + uuidChars[14] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000000f0L) >>> 4)]; + uuidChars[15] = HEX_DIGITS[(int) (mostSignificantBits & 0x000000000000000fL)]; + uuidChars[16] = HEX_DIGITS[(int) ((leastSignificantBits & 0xf000000000000000L) >>> 60)]; + uuidChars[17] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0f00000000000000L) >>> 56)]; + uuidChars[18] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00f0000000000000L) >>> 52)]; + uuidChars[19] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000f000000000000L) >>> 48)]; + uuidChars[20] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000f00000000000L) >>> 44)]; + uuidChars[21] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000f0000000000L) >>> 40)]; + uuidChars[22] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000f000000000L) >>> 36)]; + uuidChars[23] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000f00000000L) >>> 32)]; + uuidChars[24] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000f0000000L) >>> 28)]; + uuidChars[25] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000f000000L) >>> 24)]; + uuidChars[26] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000f00000L) >>> 20)]; + uuidChars[27] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000f0000L) >>> 16)]; + uuidChars[28] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000000f000L) >>> 12)]; + uuidChars[29] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000000f00L) >>> 8)]; + uuidChars[30] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000000f0L) >>> 4)]; + uuidChars[31] = HEX_DIGITS[(int) (leastSignificantBits & 0x000000000000000fL)]; + + return new String(uuidChars); + } + + private static long getHexValueForChar(final char c) { + try { + if (HEX_VALUES[c] < 0) { + throw new IllegalArgumentException("Illegal hexadecimal digit: " + c); + } + } catch (final ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Illegal hexadecimal digit: " + c); + } + + return HEX_VALUES[c]; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java b/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java index 7e443c7b5..dfe05af1c 100644 --- a/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java +++ b/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java @@ -1,7 +1,6 @@ package com.velocitypowered.api.util; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.UUID; @@ -23,11 +22,7 @@ public final class UuidUtils { */ public static UUID fromUndashed(final String string) { Objects.requireNonNull(string, "string"); - Preconditions.checkArgument(string.length() == 32, "Length is incorrect"); - return new UUID( - Long.parseUnsignedLong(string.substring(0, 16), 16), - Long.parseUnsignedLong(string.substring(16), 16) - ); + return FastUuidSansHyphens.parseUuid(string); } /** @@ -38,10 +33,7 @@ public final class UuidUtils { */ public static String toUndashed(final UUID uuid) { Preconditions.checkNotNull(uuid, "uuid"); - String msbStr = Long.toHexString(uuid.getMostSignificantBits()); - String lsbStr = Long.toHexString(uuid.getLeastSignificantBits()); - return Strings.padStart(msbStr, 16, '0') + Strings.padStart(lsbStr, - 16, '0'); + return FastUuidSansHyphens.toString(uuid); } /** diff --git a/build.gradle b/build.gradle index 8d9b45507..0ff6be509 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ allprojects { junitVersion = '5.3.0-M1' slf4jVersion = '1.7.25' log4jVersion = '2.11.2' - nettyVersion = '4.1.38.Final' + nettyVersion = '4.1.43.Final' guavaVersion = '25.1-jre' checkerFrameworkVersion = '2.7.0' configurateVersion = '3.6' diff --git a/native/src/main/java/com/velocitypowered/natives/Native.java b/native/src/main/java/com/velocitypowered/natives/Native.java index 26cdd0ba4..e90ccf80a 100644 --- a/native/src/main/java/com/velocitypowered/natives/Native.java +++ b/native/src/main/java/com/velocitypowered/natives/Native.java @@ -1,5 +1,7 @@ package com.velocitypowered.natives; +import com.velocitypowered.natives.util.BufferPreference; + public interface Native { - boolean isNative(); + BufferPreference preferredBufferType(); } diff --git a/native/src/main/java/com/velocitypowered/natives/compression/Java11VelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/Java11VelocityCompressor.java new file mode 100644 index 000000000..999d12caa --- /dev/null +++ b/native/src/main/java/com/velocitypowered/natives/compression/Java11VelocityCompressor.java @@ -0,0 +1,139 @@ +package com.velocitypowered.natives.compression; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFER_SIZE; +import static com.velocitypowered.natives.compression.CompressorUtils.ensureMaxSize; + +import com.velocitypowered.natives.util.BufferPreference; +import io.netty.buffer.ByteBuf; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +public class Java11VelocityCompressor implements VelocityCompressor { + + public static final VelocityCompressorFactory FACTORY = Java11VelocityCompressor::new; + + // The use of MethodHandle is intentional. Velocity targets Java 8, and these methods don't exist + // in Java 8. This was also the most performant solution I could find, only slightly slower than a + // direct method call without long warmup times, requiring bytecode generation through ASM, or + // other stuff. + private static final MethodHandle DEFLATE_SET_INPUT; + private static final MethodHandle INFLATE_SET_INPUT; + private static final MethodHandle DEFLATE_CALL; + private static final MethodHandle INFLATE_CALL; + + static { + try { + DEFLATE_SET_INPUT = MethodHandles.lookup().findVirtual(Deflater.class, "setInput", + MethodType.methodType(void.class, ByteBuffer.class)); + INFLATE_SET_INPUT = MethodHandles.lookup().findVirtual(Inflater.class, "setInput", + MethodType.methodType(void.class, ByteBuffer.class)); + + DEFLATE_CALL = MethodHandles.lookup().findVirtual(Deflater.class, "deflate", + MethodType.methodType(int.class, ByteBuffer.class)); + INFLATE_CALL = MethodHandles.lookup().findVirtual(Inflater.class, "inflate", + MethodType.methodType(int.class, ByteBuffer.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError("Can't use Java 11 compressor on your version of Java"); + } + } + + private final Deflater deflater; + private final Inflater inflater; + private boolean disposed = false; + + private Java11VelocityCompressor(int level) { + this.deflater = new Deflater(level); + this.inflater = new Inflater(); + } + + @Override + public void inflate(ByteBuf source, ByteBuf destination, int max) 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"); + + try { + int origIdx = source.readerIndex(); + INFLATE_SET_INPUT.invokeExact(inflater, source.nioBuffer()); + + while (!inflater.finished() && inflater.getBytesRead() < source.readableBytes()) { + if (!destination.isWritable()) { + ensureMaxSize(destination, max); + destination.ensureWritable(ZLIB_BUFFER_SIZE); + } + + ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), + destination.writableBytes()); + int produced = (int) INFLATE_CALL.invokeExact(inflater, destNioBuf); + source.readerIndex(origIdx + inflater.getTotalIn()); + destination.writerIndex(destination.writerIndex() + produced); + } + + inflater.reset(); + } catch (Throwable e) { + if (e instanceof DataFormatException) { + throw (DataFormatException) e; + } + throw new RuntimeException(e); + } + } + + @Override + public void deflate(ByteBuf source, ByteBuf destination) 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"); + + try { + int origIdx = source.readerIndex(); + DEFLATE_SET_INPUT.invokeExact(deflater, source.nioBuffer()); + deflater.finish(); + + while (!deflater.finished()) { + if (!destination.isWritable()) { + destination.ensureWritable(ZLIB_BUFFER_SIZE); + } + + ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), + destination.writableBytes()); + int produced = (int) DEFLATE_CALL.invokeExact(deflater, destNioBuf); + source.readerIndex(origIdx + deflater.getTotalIn()); + destination.writerIndex(destination.writerIndex() + produced); + } + + deflater.reset(); + } catch (Throwable e) { + if (e instanceof DataFormatException) { + throw (DataFormatException) e; + } + throw new RuntimeException(e); + } + } + + @Override + public void dispose() { + disposed = true; + deflater.end(); + inflater.end(); + } + + private void ensureNotDisposed() { + checkState(!disposed, "Object already disposed"); + } + + @Override + public BufferPreference preferredBufferType() { + return BufferPreference.DIRECT_PREFERRED; + } +} 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 633fc4f1d..6293dd770 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java @@ -4,6 +4,7 @@ import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFE import static com.velocitypowered.natives.compression.CompressorUtils.ensureMaxSize; import com.google.common.base.Preconditions; +import com.velocitypowered.natives.util.BufferPreference; import io.netty.buffer.ByteBuf; import java.util.zip.DataFormatException; import java.util.zip.Deflater; @@ -15,19 +16,37 @@ public class JavaVelocityCompressor implements VelocityCompressor { private final Deflater deflater; private final Inflater inflater; - private final byte[] buf; + private byte[] buf = new byte[0]; private boolean disposed = false; private JavaVelocityCompressor(int level) { this.deflater = new Deflater(level); this.inflater = new Inflater(); - this.buf = new byte[ZLIB_BUFFER_SIZE]; } @Override public void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException { ensureNotDisposed(); + final int available = source.readableBytes(); + this.setInflaterInput(source); + + if (destination.hasArray()) { + this.inflateDestinationIsHeap(destination, available, max); + } else { + if (buf.length == 0) { + buf = new byte[ZLIB_BUFFER_SIZE]; + } + while (!inflater.finished() && inflater.getBytesRead() < available) { + ensureMaxSize(destination, max); + int read = inflater.inflate(buf); + destination.writeBytes(buf, 0, read); + } + } + inflater.reset(); + } + + private void setInflaterInput(ByteBuf source) { final int available = source.readableBytes(); if (source.hasArray()) { inflater.setInput(source.array(), source.arrayOffset() + source.readerIndex(), available); @@ -36,19 +55,45 @@ public class JavaVelocityCompressor implements VelocityCompressor { source.readBytes(inData); inflater.setInput(inData); } + } + private void inflateDestinationIsHeap(ByteBuf destination, int available, int max) + throws DataFormatException { while (!inflater.finished() && inflater.getBytesRead() < available) { + if (!destination.isWritable()) { + ensureMaxSize(destination, max); + destination.ensureWritable(ZLIB_BUFFER_SIZE); + } + ensureMaxSize(destination, max); - int read = inflater.inflate(buf); - destination.writeBytes(buf, 0, read); + int produced = inflater.inflate(destination.array(), destination.arrayOffset() + + destination.writerIndex(), destination.writableBytes()); + destination.writerIndex(destination.writerIndex() + produced); } - inflater.reset(); } @Override public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException { ensureNotDisposed(); + this.setDeflaterInput(source); + deflater.finish(); + + if (destination.hasArray()) { + this.deflateDestinationIsHeap(destination); + } else { + if (buf.length == 0) { + buf = new byte[ZLIB_BUFFER_SIZE]; + } + while (!deflater.finished()) { + int bytes = deflater.deflate(buf); + destination.writeBytes(buf, 0, bytes); + } + } + deflater.reset(); + } + + private void setDeflaterInput(ByteBuf source) { if (source.hasArray()) { deflater.setInput(source.array(), source.arrayOffset() + source.readerIndex(), source.readableBytes()); @@ -57,12 +102,18 @@ public class JavaVelocityCompressor implements VelocityCompressor { source.readBytes(inData); deflater.setInput(inData); } - deflater.finish(); + } + + private void deflateDestinationIsHeap(ByteBuf destination) { while (!deflater.finished()) { - int bytes = deflater.deflate(buf); - destination.writeBytes(buf, 0, bytes); + if (!destination.isWritable()) { + destination.ensureWritable(ZLIB_BUFFER_SIZE); + } + + int produced = deflater.deflate(destination.array(), destination.arrayOffset() + + destination.writerIndex(), destination.writableBytes()); + destination.writerIndex(destination.writerIndex() + produced); } - deflater.reset(); } @Override @@ -77,7 +128,7 @@ public class JavaVelocityCompressor implements VelocityCompressor { } @Override - public boolean isNative() { - return false; + public BufferPreference preferredBufferType() { + return BufferPreference.HEAP_PREFERRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java index 54b6daf62..b932579ae 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/NativeVelocityCompressor.java @@ -4,6 +4,7 @@ import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFE import static com.velocitypowered.natives.compression.CompressorUtils.ensureMaxSize; import com.google.common.base.Preconditions; +import com.velocitypowered.natives.util.BufferPreference; import io.netty.buffer.ByteBuf; import java.util.zip.DataFormatException; @@ -82,7 +83,7 @@ public class NativeVelocityCompressor implements VelocityCompressor { } @Override - public boolean isNative() { - return true; + public BufferPreference preferredBufferType() { + return BufferPreference.DIRECT_REQUIRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java index 5382da898..70ead996d 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java @@ -1,6 +1,7 @@ package com.velocitypowered.natives.encryption; import com.google.common.base.Preconditions; +import com.velocitypowered.natives.util.BufferPreference; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; @@ -34,50 +35,25 @@ public class JavaVelocityCipher implements VelocityCipher { } @Override - public void process(ByteBuf source, ByteBuf destination) throws ShortBufferException { + public void process(ByteBuf source) { ensureNotDisposed(); + Preconditions.checkArgument(source.hasArray(), "No source array"); int inBytes = source.readableBytes(); - byte[] asBytes = ByteBufUtil.getBytes(source); + int baseOffset = source.arrayOffset() + source.readerIndex(); - int outputSize = cipher.getOutputSize(inBytes); - byte[] outBuf = new byte[outputSize]; - cipher.update(asBytes, 0, inBytes, outBuf); - destination.writeBytes(outBuf); - } - - @Override - public ByteBuf process(ChannelHandlerContext ctx, ByteBuf source) throws ShortBufferException { - ensureNotDisposed(); - - int inBytes = source.readableBytes(); - ByteBuf asHeapBuf = toHeap(source); - ByteBuf out = ctx.alloc().heapBuffer(cipher.getOutputSize(inBytes)); try { - out.writerIndex( - cipher.update(asHeapBuf.array(), asHeapBuf.arrayOffset() + asHeapBuf.readerIndex(), - inBytes, out.array(), out.arrayOffset() + out.writerIndex())); - return out; - } catch (ShortBufferException e) { - out.release(); - throw e; - } finally { - asHeapBuf.release(); + cipher.update(source.array(), baseOffset, inBytes, source.array(), baseOffset); + } catch (ShortBufferException ex) { + /* This _really_ shouldn't happen - AES CFB8 will work in place. + If you run into this, that means that for whatever reason the Java Runtime has determined + that the output buffer needs more bytes than the input buffer. When we are working with + AES-CFB8, the output size is equal to the input size. See the problem? */ + throw new AssertionError("Cipher update did not operate in place and requested a larger " + + "buffer than the source buffer"); } } - private static ByteBuf toHeap(ByteBuf src) { - if (src.hasArray()) { - return src.retain(); - } - - // Copy into a temporary heap buffer. We could use a local buffer, but Netty pools all buffers, - // so we'd lose more than we gain. - ByteBuf asHeapBuf = src.alloc().heapBuffer(src.readableBytes()); - asHeapBuf.writeBytes(src); - return asHeapBuf; - } - @Override public void dispose() { disposed = true; @@ -88,7 +64,7 @@ public class JavaVelocityCipher implements VelocityCipher { } @Override - public boolean isNative() { - return false; + public BufferPreference preferredBufferType() { + return BufferPreference.HEAP_REQUIRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java index b65501ff0..ee7ab1532 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java @@ -1,6 +1,7 @@ package com.velocitypowered.natives.encryption; import com.google.common.base.Preconditions; +import com.velocitypowered.natives.util.BufferPreference; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import java.security.GeneralSecurityException; @@ -32,40 +33,14 @@ public class NativeVelocityCipher implements VelocityCipher { } @Override - public void process(ByteBuf source, ByteBuf destination) throws ShortBufferException { + public void process(ByteBuf source) { ensureNotDisposed(); source.memoryAddress(); - destination.memoryAddress(); - // The exact amount we read in is also the amount we write out. + long base = source.memoryAddress() + source.readerIndex(); int len = source.readableBytes(); - destination.ensureWritable(len); - impl.process(ctx, source.memoryAddress() + source.readerIndex(), len, - destination.memoryAddress() + destination.writerIndex(), encrypt); - - source.skipBytes(len); - destination.writerIndex(destination.writerIndex() + len); - } - - @Override - public ByteBuf process(ChannelHandlerContext ctx, ByteBuf source) throws ShortBufferException { - ensureNotDisposed(); - source.memoryAddress(); // sanity check - - int len = source.readableBytes(); - ByteBuf out = ctx.alloc().directBuffer(len); - - try { - impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len, - out.memoryAddress(), encrypt); - source.skipBytes(len); - out.writerIndex(len); - return out; - } catch (Exception e) { - out.release(); - throw e; - } + impl.process(ctx, base, len, base, encrypt); } @Override @@ -81,7 +56,7 @@ public class NativeVelocityCipher implements VelocityCipher { } @Override - public boolean isNative() { - return true; + public BufferPreference preferredBufferType() { + return BufferPreference.DIRECT_REQUIRED; } } diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/VelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/VelocityCipher.java index c52c88737..3f17a6604 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/VelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/VelocityCipher.java @@ -7,8 +7,5 @@ import io.netty.channel.ChannelHandlerContext; import javax.crypto.ShortBufferException; public interface VelocityCipher extends Disposable, Native { - - void process(ByteBuf source, ByteBuf destination) throws ShortBufferException; - - ByteBuf process(ChannelHandlerContext ctx, ByteBuf source) throws ShortBufferException; + void process(ByteBuf source); } diff --git a/native/src/main/java/com/velocitypowered/natives/util/BufferPreference.java b/native/src/main/java/com/velocitypowered/natives/util/BufferPreference.java new file mode 100644 index 000000000..6814b11fd --- /dev/null +++ b/native/src/main/java/com/velocitypowered/natives/util/BufferPreference.java @@ -0,0 +1,20 @@ +package com.velocitypowered.natives.util; + +public enum BufferPreference { + /** + * A heap buffer is required. + */ + HEAP_REQUIRED, + /** + * A heap buffer is preferred (but not required). + */ + HEAP_PREFERRED, + /** + * A direct buffer is preferred (but not required). + */ + DIRECT_PREFERRED, + /** + * A direct buffer is required. + */ + DIRECT_REQUIRED +} diff --git a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java index c91e6c942..dd41e3fc7 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java +++ b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java @@ -20,28 +20,30 @@ public class MoreByteBufUtils { * @return a buffer compatible with the native */ public static ByteBuf ensureCompatible(ByteBufAllocator alloc, Native nativeStuff, ByteBuf buf) { - if (!nativeStuff.isNative() || buf.hasMemoryAddress()) { - // Will always work in either case. JNI code demands a memory address, and if we have a Java - // fallback, it uses byte arrays in all cases. + if (isCompatible(nativeStuff, buf)) { return buf.retain(); } // It's not, so we must make a direct copy. - ByteBuf newBuf = alloc.directBuffer(buf.readableBytes()); + ByteBuf newBuf = preferredBuffer(alloc, nativeStuff, buf.readableBytes()); newBuf.writeBytes(buf); return newBuf; } - /** - * Creates a {@link ByteBuf} that will have the best performance with the specified - * {@code nativeStuff}. - * - * @param alloc the {@link ByteBufAllocator} to use - * @param nativeStuff the native we are working with - * @return a buffer compatible with the native - */ - public static ByteBuf preferredBuffer(ByteBufAllocator alloc, Native nativeStuff) { - return nativeStuff.isNative() ? alloc.directBuffer() : alloc.heapBuffer(); + private static boolean isCompatible(Native nativeStuff, ByteBuf buf) { + BufferPreference preferred = nativeStuff.preferredBufferType(); + switch (preferred) { + case DIRECT_PREFERRED: + case HEAP_PREFERRED: + // The native prefers this type, but doesn't strictly require we provide it. + return true; + case DIRECT_REQUIRED: + return buf.hasMemoryAddress(); + case HEAP_REQUIRED: + return buf.hasArray(); + default: + throw new AssertionError("Preferred buffer type unknown"); + } } /** @@ -55,7 +57,15 @@ public class MoreByteBufUtils { */ public static ByteBuf preferredBuffer(ByteBufAllocator alloc, Native nativeStuff, int initialCapacity) { - return nativeStuff.isNative() ? alloc.directBuffer(initialCapacity) : alloc - .heapBuffer(initialCapacity); + switch (nativeStuff.preferredBufferType()) { + case HEAP_REQUIRED: + case HEAP_PREFERRED: + return alloc.heapBuffer(initialCapacity); + case DIRECT_PREFERRED: + case DIRECT_REQUIRED: + return alloc.directBuffer(initialCapacity); + default: + throw new AssertionError("Preferred buffer type unknown"); + } } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java b/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java index 494b352ad..e4da84f73 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java @@ -15,7 +15,7 @@ public final class NativeCodeLoader implements Supplier { @Override public T get() { - return selected.object; + return selected.constructed; } private static Variant getVariant(List> variants) { @@ -38,9 +38,14 @@ public final class NativeCodeLoader implements Supplier { private Status status; private final Runnable setup; private final String name; - private final T object; + private final Supplier object; + private T constructed; Variant(BooleanSupplier possiblyAvailable, Runnable setup, String name, T object) { + this(possiblyAvailable, setup, name, () -> object); + } + + Variant(BooleanSupplier possiblyAvailable, Runnable setup, String name, Supplier object) { this.status = possiblyAvailable.getAsBoolean() ? Status.POSSIBLY_AVAILABLE : Status.NOT_AVAILABLE; this.setup = setup; @@ -57,6 +62,7 @@ public final class NativeCodeLoader implements Supplier { if (status == Status.POSSIBLY_AVAILABLE) { try { setup.run(); + constructed = object.get(); status = Status.SETUP; } catch (Exception e) { status = Status.SETUP_FAILURE; @@ -64,7 +70,7 @@ public final class NativeCodeLoader implements Supplier { } } - return object; + return constructed; } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java index bf5b73002..6e1bd1c3f 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java @@ -6,6 +6,7 @@ import java.util.function.BooleanSupplier; public class NativeConstraints { private static final boolean NATIVES_ENABLED = !Boolean.getBoolean("velocity.natives-disabled"); + private static final boolean IS_AMD64; private static final boolean CAN_GET_MEMORYADDRESS; static { @@ -15,19 +16,28 @@ public class NativeConstraints { } finally { test.release(); } + + String osArch = System.getProperty("os.arch", ""); + // HotSpot on Intel macOS prefers x86_64, but OpenJ9 on macOS and HotSpot/OpenJ9 elsewhere + // give amd64. + IS_AMD64 = osArch.equals("amd64") || osArch.equals("x86_64"); } static final BooleanSupplier MACOS = () -> { return NATIVES_ENABLED && CAN_GET_MEMORYADDRESS && System.getProperty("os.name", "").equalsIgnoreCase("Mac OS X") - && System.getProperty("os.arch", "").equals("x86_64"); + && IS_AMD64; }; static final BooleanSupplier LINUX = () -> { return NATIVES_ENABLED && CAN_GET_MEMORYADDRESS && System.getProperty("os.name", "").equalsIgnoreCase("Linux") - && System.getProperty("os.arch", "").equals("amd64"); + && IS_AMD64; + }; + + static final BooleanSupplier JAVA_11 = () -> { + return Double.parseDouble(System.getProperty("java.specification.version")) >= 11; }; } diff --git a/native/src/main/java/com/velocitypowered/natives/util/Natives.java b/native/src/main/java/com/velocitypowered/natives/util/Natives.java index 44329434a..7e4f05e97 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/Natives.java +++ b/native/src/main/java/com/velocitypowered/natives/util/Natives.java @@ -2,6 +2,7 @@ package com.velocitypowered.natives.util; import com.google.common.collect.ImmutableList; import com.velocitypowered.natives.NativeSetupException; +import com.velocitypowered.natives.compression.Java11VelocityCompressor; import com.velocitypowered.natives.compression.JavaVelocityCompressor; import com.velocitypowered.natives.compression.NativeVelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressorFactory; @@ -52,6 +53,8 @@ public class Natives { new NativeCodeLoader.Variant<>(NativeConstraints.LINUX, copyAndLoadNative("/linux_x64/velocity-compress.so"), "native (Linux amd64)", NativeVelocityCompressor.FACTORY), + new NativeCodeLoader.Variant<>(NativeConstraints.JAVA_11, () -> { + }, "Java 11", () -> Java11VelocityCompressor.FACTORY), new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> { }, "Java", JavaVelocityCompressor.FACTORY) ) diff --git a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java index 40f74999e..182423e98 100644 --- a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java +++ b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.condition.OS.LINUX; import static org.junit.jupiter.api.condition.OS.MAC; +import com.velocitypowered.natives.util.BufferPreference; import com.velocitypowered.natives.util.Natives; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -17,7 +18,9 @@ import java.util.zip.DataFormatException; import java.util.zip.Deflater; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.JRE; class VelocityCompressorTest { @@ -39,7 +42,7 @@ class VelocityCompressorTest { @EnabledOnOs({MAC, LINUX}) void nativeIntegrityCheck() throws DataFormatException { VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION); - if (compressor instanceof JavaVelocityCompressor) { + if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) { compressor.dispose(); fail("Loaded regular compressor"); } @@ -60,6 +63,22 @@ class VelocityCompressorTest { check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); } + @Test + @EnabledOnJre(JRE.JAVA_11) + void java11IntegrityCheckDirect() throws DataFormatException { + VelocityCompressor compressor = Java11VelocityCompressor.FACTORY + .create(Deflater.DEFAULT_COMPRESSION); + check(compressor, () -> Unpooled.directBuffer(TEST_DATA.length + 32)); + } + + @Test + @EnabledOnJre(JRE.JAVA_11) + void java11IntegrityCheckHeap() throws DataFormatException { + VelocityCompressor compressor = Java11VelocityCompressor.FACTORY + .create(Deflater.DEFAULT_COMPRESSION); + check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); + } + private void check(VelocityCompressor compressor, Supplier bufSupplier) throws DataFormatException { ByteBuf source = bufSupplier.get(); diff --git a/native/src/test/java/com/velocitypowered/natives/encryption/VelocityCipherTest.java b/native/src/test/java/com/velocitypowered/natives/encryption/VelocityCipherTest.java index d7328c922..93ab89117 100644 --- a/native/src/test/java/com/velocitypowered/natives/encryption/VelocityCipherTest.java +++ b/native/src/test/java/com/velocitypowered/natives/encryption/VelocityCipherTest.java @@ -56,20 +56,18 @@ class VelocityCipherTest { VelocityCipher encrypt = factory.forEncryption(new SecretKeySpec(AES_KEY, "AES")); ByteBuf source = bufSupplier.get(); - ByteBuf dest = bufSupplier.get(); - ByteBuf decryptionBuf = bufSupplier.get(); source.writeBytes(TEST_DATA); + ByteBuf workingBuf = source.copy(); + try { - encrypt.process(source, dest); - decrypt.process(dest, decryptionBuf); - source.readerIndex(0); - assertTrue(ByteBufUtil.equals(source, decryptionBuf)); + encrypt.process(workingBuf); + decrypt.process(workingBuf); + assertTrue(ByteBufUtil.equals(source, workingBuf)); } finally { source.release(); - dest.release(); - decryptionBuf.release(); + workingBuf.release(); decrypt.dispose(); encrypt.dispose(); } diff --git a/proxy/build.gradle b/proxy/build.gradle index 9c5bf26fb..216e58cc8 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -48,6 +48,8 @@ dependencies { compile "io.netty:netty-handler:${nettyVersion}" compile "io.netty:netty-transport-native-epoll:${nettyVersion}" compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64" + compile "io.netty:netty-transport-native-kqueue:${nettyVersion}" + compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64" compile "io.netty:netty-resolver-dns:${nettyVersion}" compile "org.apache.logging.log4j:log4j-api:${log4jVersion}" @@ -65,7 +67,7 @@ dependencies { compile 'com.mojang:brigadier:1.0.15' - compile 'org.asynchttpclient:async-http-client:2.10.1' + compile 'org.asynchttpclient:async-http-client:2.10.4' compile 'com.spotify:completable-futures:0.3.2' diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java index a67582a0a..1c3b777c3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java @@ -72,7 +72,10 @@ public class VelocityCommand implements Command { @Override public List suggest(CommandSource source, String @NonNull [] currentArgs) { if (currentArgs.length == 0) { - return ImmutableList.copyOf(subcommands.keySet()); + return subcommands.entrySet().stream() + .filter(e -> e.getValue().hasPermission(source, new String[0])) + .map(Map.Entry::getKey) + .collect(ImmutableList.toImmutableList()); } if (currentArgs.length == 1) { @@ -81,7 +84,7 @@ public class VelocityCommand implements Command { currentArgs[0].length())) .filter(e -> e.getValue().hasPermission(source, new String[0])) .map(Map.Entry::getKey) - .collect(Collectors.toList()); + .collect(ImmutableList.toImmutableList()); } Command command = subcommands.get(currentArgs[0].toLowerCase(Locale.US)); 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 6d8c24f4d..467363daf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -100,7 +100,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi " configuration is used if no servers could be contacted." }) @ConfigKey("ping-passthrough") - private PingPassthroughMode pingPassthrough; + private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; @Table("[servers]") private final Servers servers; @@ -192,44 +192,38 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi } if (servers.getServers().isEmpty()) { - logger.error("You have no servers configured. :("); - valid = false; - } else { - if (servers.getAttemptConnectionOrder().isEmpty()) { - logger.error("No fallback servers are configured!"); + logger.warn("You don't have any servers configured."); + } + + for (Map.Entry entry : servers.getServers().entrySet()) { + try { + AddressUtil.parseAddress(entry.getValue()); + } catch (IllegalArgumentException e) { + logger.error("Server {} does not have a valid IP address.", entry.getKey(), e); valid = false; } + } - for (Map.Entry entry : servers.getServers().entrySet()) { - try { - AddressUtil.parseAddress(entry.getValue()); - } catch (IllegalArgumentException e) { - logger.error("Server {} does not have a valid IP address.", entry.getKey(), e); - valid = false; - } + for (String s : servers.getAttemptConnectionOrder()) { + if (!servers.getServers().containsKey(s)) { + logger.error("Fallback server " + s + " is not registered in your configuration!"); + valid = false; + } + } + + for (Map.Entry> entry : forcedHosts.getForcedHosts().entrySet()) { + if (entry.getValue().isEmpty()) { + logger.error("Forced host '{}' does not contain any servers", entry.getKey()); + valid = false; + continue; } - for (String s : servers.getAttemptConnectionOrder()) { - if (!servers.getServers().containsKey(s)) { - logger.error("Fallback server " + s + " is not registered in your configuration!"); + for (String server : entry.getValue()) { + if (!servers.getServers().containsKey(server)) { + logger.error("Server '{}' for forced host '{}' does not exist", server, entry.getKey()); valid = false; } } - - for (Map.Entry> entry : forcedHosts.getForcedHosts().entrySet()) { - if (entry.getValue().isEmpty()) { - logger.error("Forced host '{}' does not contain any servers", entry.getKey()); - valid = false; - continue; - } - - for (String server : entry.getValue()) { - if (!servers.getServers().containsKey(server)) { - logger.error("Server '{}' for forced host '{}' does not exist", server, entry.getKey()); - valid = false; - } - } - } } try { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java index a69768946..b6d5fc75a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java @@ -34,14 +34,4 @@ public interface ConnectionType { */ GameProfile addGameProfileTokensIfRequired(GameProfile original, PlayerInfoForwarding forwardingType); - - /** - * Tests whether the hostname is the handshake packet is valid. - * - * @param address The address to check - * @return true if valid. - */ - default boolean checkServerAddressIsValid(String address) { - return true; - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 1c6a0b05b..c5eb9d8d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -80,7 +80,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // The goods are in hand! We got JoinGame. Let's transition completely to the new state. smc.setAutoReading(false); server.getEventManager() - .fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer())) + .fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer(), + existingConnection != null ? existingConnection.getServer() : null)) .whenCompleteAsync((x, error) -> { // Strap on the ClientPlaySessionHandler if required. ClientPlaySessionHandler playHandler; 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 e0bf9dc38..a519436d1 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 @@ -304,26 +304,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Clear tab list to avoid duplicate entries player.getTabList().clearAll(); - // In order to handle switching to another server, you will need to send three packets: + // In order to handle switching to another server, you will need to send two packets: // - // - The join game packet from the backend server - // - A respawn packet with a different dimension - // - Another respawn with the correct dimension - // - // The two respawns with different dimensions are required, otherwise the client gets - // confused. + // - The join game packet from the backend server, with a different dimension + // - A respawn with the correct dimension // // Most notably, by having the client accept the join game packet, we can work around the need // to perform entity ID rewrites, eliminating potential issues from rewriting packets and // improving compatibility with mods. + int realDim = joinGame.getDimension(); + joinGame.setDimension(getFakeTemporaryDimensionId(realDim)); player.getConnection().delayedWrite(joinGame); - int tempDim = joinGame.getDimension() == 0 ? -1 : 0; player.getConnection().delayedWrite( - new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), - joinGame.getLevelType())); - player.getConnection().delayedWrite( - new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), - joinGame.getLevelType())); + new Respawn(realDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), + joinGame.getGamemode(), joinGame.getLevelType())); } // Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to @@ -360,11 +354,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { destination.completeJoin(); } + private static int getFakeTemporaryDimensionId(int dim) { + return dim == 0 ? -1 : 0; + } + public List getServerBossBars() { return serverBossBars; } - private boolean handleCommandTabComplete(TabCompleteRequest packet) { // In 1.13+, we need to do additional work for the richer suggestions available. String command = packet.getCommand().substring(1); @@ -389,31 +386,19 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } List offers = new ArrayList<>(); - int longestLength = 0; for (String suggestion : suggestions) { offers.add(new Offer(suggestion)); - if (suggestion.length() > longestLength) { - longestLength = suggestion.length(); - } } - TabCompleteResponse resp = new TabCompleteResponse(); - resp.setTransactionId(packet.getTransactionId()); - int startPos = packet.getCommand().lastIndexOf(' ') + 1; - int length; - if (startPos == 0) { - startPos = packet.getCommand().length() + 1; - length = longestLength; - } else { - length = packet.getCommand().length() - startPos; + if (startPos > 0) { + TabCompleteResponse resp = new TabCompleteResponse(); + resp.setTransactionId(packet.getTransactionId()); + resp.setStart(startPos); + resp.setLength(packet.getCommand().length() - startPos); + resp.getOffers().addAll(offers); + player.getConnection().write(resp); } - - resp.setStart(startPos); - resp.setLength(length); - resp.getOffers().addAll(offers); - - player.getConnection().write(resp); return true; } 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 0483ce352..97dc93305 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 @@ -425,33 +425,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return; } - if (connectedServer == null) { - Optional nextServer = getNextServerToTry(rs); - if (nextServer.isPresent()) { - // There can't be any connection in flight now. - resetInFlightConnection(); - createConnectionRequest(nextServer.get()).fireAndForget(); - } else { - disconnect(friendlyReason); - } + boolean kickedFromCurrent = connectedServer == null || connectedServer.getServer().equals(rs); + ServerKickResult result; + if (kickedFromCurrent) { + Optional next = getNextServerToTry(rs); + result = next.map(RedirectPlayer::create) + .orElseGet(() -> DisconnectPlayer.create(friendlyReason)); } else { - boolean kickedFromCurrent = connectedServer.getServer().equals(rs); - ServerKickResult result; - if (kickedFromCurrent) { - Optional next = getNextServerToTry(rs); - result = next.map(RedirectPlayer::create) - .orElseGet(() -> DisconnectPlayer.create(friendlyReason)); - } else { - // If we were kicked by going to another server, the connection should not be in flight - if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) { - resetInFlightConnection(); - } - result = Notify.create(friendlyReason); + // If we were kicked by going to another server, the connection should not be in flight + if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) { + resetInFlightConnection(); } - KickedFromServerEvent originalEvent = new KickedFromServerEvent(this, rs, kickReason, - !kickedFromCurrent, result); - handleKickEvent(originalEvent, friendlyReason); + result = Notify.create(friendlyReason); } + KickedFromServerEvent originalEvent = new KickedFromServerEvent(this, rs, kickReason, + !kickedFromCurrent, result); + handleKickEvent(originalEvent, friendlyReason); } private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason) { @@ -471,7 +460,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { if (newResult == null || !newResult) { disconnect(friendlyReason); } else { - sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason)); + if (res.getMessage() == null) { + sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason)); + } else { + sendMessage(res.getMessage()); + } } }, connection.eventLoop()); } else if (event.getResult() instanceof Notify) { 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 1eb25a2ba..b372010a3 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 @@ -26,9 +26,14 @@ import java.util.Optional; import net.kyori.text.TextComponent; import net.kyori.text.TranslatableComponent; import net.kyori.text.format.TextColor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; public class HandshakeSessionHandler implements MinecraftSessionHandler { + private static final Logger LOGGER = LogManager.getLogger(HandshakeSessionHandler.class); + private final MinecraftConnection connection; private final VelocityServer server; @@ -58,58 +63,72 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { public boolean handle(Handshake handshake) { InitialInboundConnection ic = new InitialInboundConnection(connection, cleanVhost(handshake.getServerAddress()), handshake); - connection.setAssociation(ic); - switch (handshake.getNextStatus()) { + StateRegistry nextState = getStateForProtocol(handshake.getNextStatus()); + if (nextState == null) { + LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus()); + connection.close(); + } else { + connection.setState(nextState); + connection.setProtocolVersion(handshake.getProtocolVersion()); + connection.setAssociation(ic); + + switch (nextState) { + case STATUS: + connection.setSessionHandler(new StatusSessionHandler(server, connection, ic)); + break; + case LOGIN: + this.handleLogin(handshake, ic); + break; + default: + // If you get this, it's a bug in Velocity. + throw new AssertionError("getStateForProtocol provided invalid state!"); + } + } + + return true; + } + + private static @Nullable StateRegistry getStateForProtocol(int status) { + switch (status) { case StateRegistry.STATUS_ID: - connection.setState(StateRegistry.STATUS); - connection.setProtocolVersion(handshake.getProtocolVersion()); - connection.setSessionHandler(new StatusSessionHandler(server, connection, ic)); - return true; + return StateRegistry.STATUS; case StateRegistry.LOGIN_ID: - connection.setState(StateRegistry.LOGIN); - connection.setProtocolVersion(handshake.getProtocolVersion()); - - if (!ProtocolVersion.isSupported(handshake.getProtocolVersion())) { - connection.closeWith(Disconnect - .create(TranslatableComponent.of("multiplayer.disconnect.outdated_client"))); - return true; - } - - InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress(); - if (!server.getIpAttemptLimiter().attempt(address)) { - connection.closeWith( - Disconnect.create(TextComponent.of("You are logging in too fast, try again later."))); - return true; - } - - ConnectionType type = checkForForge(handshake); - connection.setType(type); - - // Make sure legacy forwarding is not in use on this connection. - if (!type.checkServerAddressIsValid(handshake.getServerAddress())) { - connection.closeWith(Disconnect - .create(TextComponent.of("Running Velocity behind Velocity is unsupported."))); - return true; - } - - // If the proxy is configured for modern forwarding, we must deny connections from 1.12.2 - // and lower, otherwise IP information will never get forwarded. - if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN - && handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - connection.closeWith(Disconnect - .create(TextComponent.of("This server is only compatible with 1.13 and above."))); - return true; - } - - server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic)); - connection.setSessionHandler(new LoginSessionHandler(server, connection, ic)); - return true; + return StateRegistry.LOGIN; default: - throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus()); + return null; } } - private ConnectionType checkForForge(Handshake handshake) { + private void handleLogin(Handshake handshake, InitialInboundConnection ic) { + if (!ProtocolVersion.isSupported(handshake.getProtocolVersion())) { + connection.closeWith(Disconnect + .create(TranslatableComponent.of("multiplayer.disconnect.outdated_client"))); + return; + } + + InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress(); + if (!server.getIpAttemptLimiter().attempt(address)) { + connection.closeWith( + Disconnect.create(TextComponent.of("You are logging in too fast, try again later."))); + return; + } + + connection.setType(getHandshakeConnectionType(handshake)); + + // If the proxy is configured for modern forwarding, we must deny connections from 1.12.2 + // and lower, otherwise IP information will never get forwarded. + if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN + && handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { + connection.closeWith(Disconnect + .create(TextComponent.of("This server is only compatible with 1.13 and above."))); + return; + } + + server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic)); + connection.setSessionHandler(new LoginSessionHandler(server, connection, ic)); + } + + private ConnectionType getHandshakeConnectionType(Handshake handshake) { // Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13). if (handshake.getServerAddress().endsWith(LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN) && handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { @@ -119,8 +138,8 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { // forge handshake attempts. Also sends a reset handshake packet on every transition. return ConnectionTypes.UNDETERMINED_17; } else { - // For later: See if we can determine Forge 1.13+ here, else this will need to be UNDETERMINED - // until later in the cycle (most likely determinable during the LOGIN phase) + // Note for future implementation: Forge 1.13+ identifies itself using a slightly different + // hostname token. return ConnectionTypes.VANILLA; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index 05b032ce6..6fb58ffea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -1,24 +1,21 @@ package com.velocitypowered.proxy.connection.client; import static com.google.common.net.UrlEscapers.urlFormParameterEscaper; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.VelocityServer.GSON; import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY; -import static com.velocitypowered.proxy.connection.VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL; import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa; import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId; import static org.asynchttpclient.Dsl.asyncHttpClient; -import static org.asynchttpclient.Dsl.config; import com.google.common.base.Preconditions; -import com.google.common.net.UrlEscapers; import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; +import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; @@ -29,17 +26,12 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; import com.velocitypowered.proxy.protocol.packet.EncryptionResponse; -import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; -import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; import com.velocitypowered.proxy.protocol.packet.ServerLogin; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; import com.velocitypowered.proxy.util.VelocityMessages; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.Arrays; @@ -50,7 +42,6 @@ import java.util.concurrent.ThreadLocalRandom; import net.kyori.text.Component; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.asynchttpclient.Dsl; import org.asynchttpclient.ListenableFuture; import org.asynchttpclient.Response; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -66,7 +57,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private final InitialInboundConnection inbound; private @MonotonicNonNull ServerLogin login; private byte[] verify = EMPTY_BYTE_ARRAY; - private int playerInfoId; private @MonotonicNonNull ConnectedPlayer connectedPlayer; LoginSessionHandler(VelocityServer server, MinecraftConnection mcConnection, @@ -79,29 +69,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerLogin packet) { this.login = packet; - if (mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0) { - // To make sure the connecting client isn't Velocity, send a plugin message that Velocity will - // recognize and respond to. - playerInfoId = ThreadLocalRandom.current().nextInt(); - mcConnection.write(new LoginPluginMessage(playerInfoId, VELOCITY_IP_FORWARDING_CHANNEL, - Unpooled.EMPTY_BUFFER)); - } else { - beginPreLogin(); - } - return true; - } - - @Override - public boolean handle(LoginPluginResponse packet) { - if (packet.getId() == playerInfoId) { - if (packet.isSuccess()) { - // Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening. - inbound.disconnect(VelocityMessages.NO_PROXY_BEHIND_PROXY); - } else { - // Proceed with the regular login process. - beginPreLogin(); - } - } + beginPreLogin(); return true; } @@ -254,12 +222,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } private void finishLogin(ConnectedPlayer player) { - Optional toTry = player.getNextServerToTry(); - if (!toTry.isPresent()) { - player.disconnect(VelocityMessages.NO_AVAILABLE_SERVERS); - return; - } - int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { mcConnection.write(new SetCompression(threshold)); @@ -292,11 +254,27 @@ public class LoginSessionHandler implements MinecraftSessionHandler { mcConnection.setSessionHandler(new InitialConnectSessionHandler(player)); server.getEventManager().fire(new PostLoginEvent(player)) - .thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget()); + .thenRun(() -> connectToInitialServer(player)); } }, mcConnection.eventLoop()); } + private void connectToInitialServer(ConnectedPlayer player) { + Optional initialFromConfig = player.getNextServerToTry(); + PlayerChooseInitialServerEvent event = new PlayerChooseInitialServerEvent(player, + initialFromConfig.orElse(null)); + + server.getEventManager().fire(event) + .thenRunAsync(() -> { + Optional toTry = event.getInitialServer(); + if (!toTry.isPresent()) { + player.disconnect(VelocityMessages.NO_AVAILABLE_SERVERS); + return; + } + player.createConnectionRequest(toTry.get()).fireAndForget(); + }, mcConnection.eventLoop()); + } + @Override public void handleUnknown(ByteBuf buf) { mcConnection.close(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java index 43e25e65c..1a932c4f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java @@ -6,7 +6,7 @@ package com.velocitypowered.proxy.connection.forge.legacy; public class LegacyForgeConstants { /** - * Clients attempting to connect to 1.8+ Forge servers will have + * Clients attempting to connect to 1.8-1.12.2 Forge servers will have * this token appended to the hostname in the initial handshake * packet. */ diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java index b5829f375..0edff72c0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -7,6 +7,11 @@ import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueDatagramChannel; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.kqueue.KQueueSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; @@ -22,7 +27,11 @@ enum TransportType { (name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))), EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class, EpollDatagramChannel.class, - (name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))); + (name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))), + KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class, + KQueueDatagramChannel.class, + (name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type))); + final String name; final Class serverSocketChannelClass; @@ -62,6 +71,8 @@ enum TransportType { if (Epoll.isAvailable()) { return EPOLL; + } else if (KQueue.isAvailable()) { + return KQUEUE; } else { return NIO; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index ad7b53103..b6ce18016 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -3,6 +3,7 @@ package com.velocitypowered.proxy.plugin; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.base.Joiner; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; @@ -101,6 +102,8 @@ public class VelocityPluginManager implements PluginManager { continue; } + logger.info("Loaded plugin {} {} by {}", plugin.getId(), plugin.getVersion() + .orElse(""), Joiner.on(", ").join(plugin.getAuthors())); registerPlugin(pluginObject); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 4444722cb..efe241454 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -5,6 +5,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -120,51 +121,61 @@ public enum StateRegistry { map(0x1F, MINECRAFT_1_14, false)); clientbound.register(BossBar.class, BossBar::new, - map(0x0C, MINECRAFT_1_9, false)); + map(0x0C, MINECRAFT_1_9, false), + map(0x0D, MINECRAFT_1_15, false)); clientbound.register(Chat.class, Chat::new, map(0x02, MINECRAFT_1_7_2, true), map(0x0F, MINECRAFT_1_9, true), - map(0x0E, MINECRAFT_1_13, true)); + map(0x0E, MINECRAFT_1_13, true), + map(0x0F, MINECRAFT_1_15, true)); clientbound.register(TabCompleteResponse.class, TabCompleteResponse::new, map(0x3A, MINECRAFT_1_7_2, false), map(0x0E, MINECRAFT_1_9, false), - map(0x10, MINECRAFT_1_13, false)); + map(0x10, MINECRAFT_1_13, false), + map(0x11, MINECRAFT_1_15, false)); clientbound.register(AvailableCommands.class, AvailableCommands::new, - map(0x11, MINECRAFT_1_13, false)); + map(0x11, MINECRAFT_1_13, false), + map(0x12, MINECRAFT_1_15, false)); clientbound.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), map(0x19, MINECRAFT_1_13, false), - map(0x18, MINECRAFT_1_14, false)); + map(0x18, MINECRAFT_1_14, false), + map(0x19, MINECRAFT_1_15, false)); clientbound.register(Disconnect.class, Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), map(0x1B, MINECRAFT_1_13, false), - map(0x1A, MINECRAFT_1_14, false)); + map(0x1A, MINECRAFT_1_14, false), + map(0x1B, MINECRAFT_1_15, false)); clientbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), map(0x21, MINECRAFT_1_13, false), - map(0x20, MINECRAFT_1_14, false)); + map(0x20, MINECRAFT_1_14, false), + map(0x21, MINECRAFT_1_15, false)); clientbound.register(JoinGame.class, JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), map(0x25, MINECRAFT_1_13, false), - map(0x25, MINECRAFT_1_14, false)); + map(0x25, MINECRAFT_1_14, false), + map(0x26, MINECRAFT_1_15, false)); clientbound.register(Respawn.class, Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), map(0x34, MINECRAFT_1_12, true), map(0x35, MINECRAFT_1_12_1, true), map(0x38, MINECRAFT_1_13, true), - map(0x3A, MINECRAFT_1_14, true)); + map(0x3A, MINECRAFT_1_14, true), + map(0x3B, MINECRAFT_1_15, true)); clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, true), map(0x32, MINECRAFT_1_9, true), map(0x33, MINECRAFT_1_12, true), map(0x34, MINECRAFT_1_12_1, true), map(0x37, MINECRAFT_1_13, true), - map(0x39, MINECRAFT_1_14, true)); + map(0x39, MINECRAFT_1_14, true), + map(0x3A, MINECRAFT_1_15, true)); clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), @@ -172,20 +183,23 @@ public enum StateRegistry { map(0x49, MINECRAFT_1_12, true), map(0x4A, MINECRAFT_1_12_1, true), map(0x4E, MINECRAFT_1_13, true), - map(0x53, MINECRAFT_1_14, true)); + map(0x53, MINECRAFT_1_14, true), + map(0x54, MINECRAFT_1_15, true)); clientbound.register(TitlePacket.class, TitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_12, true), map(0x48, MINECRAFT_1_12_1, true), map(0x4B, MINECRAFT_1_13, true), - map(0x4F, MINECRAFT_1_14, true)); + map(0x4F, MINECRAFT_1_14, true), + map(0x50, MINECRAFT_1_15, true)); clientbound.register(PlayerListItem.class, PlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), map(0x2E, MINECRAFT_1_12_1, false), map(0x30, MINECRAFT_1_13, false), - map(0x33, MINECRAFT_1_14, false)); + map(0x33, MINECRAFT_1_14, false), + map(0x34, MINECRAFT_1_15, false)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java index 8bcda72de..b3407c980 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java @@ -18,6 +18,7 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import java.net.InetAddress; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -59,6 +60,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler private final Cache sessions = CacheBuilder.newBuilder() .expireAfterWrite(30, TimeUnit.SECONDS) .build(); + private final SecureRandom random; private volatile @MonotonicNonNull List pluginInformationList = null; @@ -67,6 +69,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler public GS4QueryHandler(VelocityServer server) { this.server = server; + this.random = new SecureRandom(); } private QueryResponse createInitialResponse() { @@ -111,7 +114,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler switch (type) { case QUERY_TYPE_HANDSHAKE: { // Generate new challenge token and put it into the sessions cache - int challengeToken = ThreadLocalRandom.current().nextInt(); + int challengeToken = random.nextInt(); sessions.put(senderAddress, challengeToken); // Respond with challenge token diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java index 4541fc902..ad02191e7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java @@ -5,10 +5,10 @@ import com.velocitypowered.natives.encryption.VelocityCipher; import com.velocitypowered.natives.util.MoreByteBufUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToMessageDecoder; import java.util.List; -public class MinecraftCipherDecoder extends ByteToMessageDecoder { +public class MinecraftCipherDecoder extends MessageToMessageDecoder { private final VelocityCipher cipher; @@ -18,16 +18,19 @@ public class MinecraftCipherDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), cipher, in); + ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), cipher, in).slice(); try { - out.add(cipher.process(ctx, compatible)); - } finally { - compatible.release(); + cipher.process(compatible); + out.add(compatible); + in.skipBytes(in.readableBytes()); + } catch (Exception e) { + compatible.release(); // compatible will never be used if we throw an exception + throw e; } } @Override - protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { cipher.dispose(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java index dd944a19f..ecaf9adb6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java @@ -20,9 +20,11 @@ public class MinecraftCipherEncoder extends MessageToMessageEncoder { protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), cipher, msg); try { - out.add(cipher.process(ctx, compatible)); - } finally { - compatible.release(); + cipher.process(compatible); + out.add(compatible); + } catch (Exception e) { + compatible.release(); // compatible will never be used if we throw an exception + throw e; } } 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 index 9a040dec4..ebf7f927e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java @@ -13,7 +13,8 @@ import java.util.List; public class MinecraftCompressDecoder extends MessageToMessageDecoder { - private static final int MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB + private static final int SOFT_MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB + private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB private final int threshold; private final VelocityCompressor compressor; @@ -25,21 +26,23 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - int expectedSize = ProtocolUtils.readVarInt(in); - if (expectedSize == 0) { + int claimedUncompressedSize = ProtocolUtils.readVarInt(in); + if (claimedUncompressedSize == 0) { // Strip the now-useless uncompressed size, this message is already uncompressed. out.add(in.retainedSlice()); in.skipBytes(in.readableBytes()); return; } - checkFrame(expectedSize >= threshold, "Uncompressed size %s is less than threshold %s", - expectedSize, threshold); - int initialCapacity = Math.min(expectedSize, MAXIMUM_UNCOMPRESSED_SIZE); + checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than" + + " threshold %s", claimedUncompressedSize, threshold); + int allowedMax = Math.min(claimedUncompressedSize, HARD_MAXIMUM_UNCOMPRESSED_SIZE); + int initialCapacity = Math.min(claimedUncompressedSize, SOFT_MAXIMUM_UNCOMPRESSED_SIZE); + ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in); ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity); try { - compressor.inflate(compatibleIn, uncompressed, expectedSize); + compressor.inflate(compatibleIn, uncompressed, allowedMax); out.add(uncompressed); } catch (Exception e) { uncompressed.release(); 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 a6f616941..2cf96e84b 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 @@ -47,20 +47,21 @@ public class MinecraftDecoder extends MessageToMessageDecoder { packet.decode(msg, direction, registry.version); } catch (Exception e) { throw new CorruptedFrameException( - "Error decoding " + packet.getClass() + " Direction " + direction - + " Protocol " + registry.version + " State " + state + " ID " + Integer - .toHexString(packetId), e); + "Error decoding " + packet.getClass() + " " + getExtraConnectionDetail(packetId), e); } if (msg.isReadable()) { - throw new CorruptedFrameException( - "Did not read full packet for " + packet.getClass() + " Direction " + direction - + " Protocol " + registry.version + " State " + state + " ID " + Integer - .toHexString(packetId)); + throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " " + + getExtraConnectionDetail(packetId)); } out.add(packet); } } + private String getExtraConnectionDetail(int packetId) { + return "Direction " + direction + " Protocol " + registry.version + " State " + state + + " ID " + Integer.toHexString(packetId); + } + public void setProtocolVersion(ProtocolVersion protocolVersion) { this.registry = direction.getProtocolRegistry(state, protocolVersion); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java index 6a114075c..dd1fa5aa1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java @@ -27,16 +27,13 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { // Make sure reader index of length buffer is returned to the beginning in.readerIndex(origReaderIndex); int packetLength = ProtocolUtils.readVarInt(in); - if (packetLength == 0) { - return; - } - if (in.readableBytes() < packetLength) { + if (in.readableBytes() >= packetLength) { + out.add(in.readRetainedSlice(packetLength)); + } else { in.readerIndex(origReaderIndex); - return; } - out.add(in.readRetainedSlice(packetLength)); return; } } 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 44d1b8847..14c6ade56 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 @@ -1,5 +1,7 @@ package com.velocitypowered.proxy.protocol.netty; +import com.velocitypowered.natives.encryption.JavaVelocityCipher; +import com.velocitypowered.natives.util.Natives; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; @@ -11,6 +13,7 @@ import java.util.List; public class MinecraftVarintLengthEncoder extends MessageToMessageEncoder { public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder(); + private static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY; private MinecraftVarintLengthEncoder() { } @@ -18,7 +21,7 @@ public class MinecraftVarintLengthEncoder extends MessageToMessageEncoder list) throws Exception { - ByteBuf lengthBuf = ctx.alloc().buffer(5); // the maximum size of a varint + ByteBuf lengthBuf = IS_JAVA_CIPHER ? ctx.alloc().heapBuffer(5) : ctx.alloc().directBuffer(5); ProtocolUtils.writeVarInt(lengthBuf, buf.readableBytes()); list.add(lengthBuf); list.add(buf.retain()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index 0b542803f..634fd1c4c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -12,11 +12,13 @@ public class JoinGame implements MinecraftPacket { private int entityId; private short gamemode; private int dimension; + private long partialHashedSeed; // 1.15+ private short difficulty; private short maxPlayers; private @Nullable String levelType; private int viewDistance; //1.14+ private boolean reducedDebugInfo; + private boolean mystery; public int getEntityId() { return entityId; @@ -42,6 +44,10 @@ public class JoinGame implements MinecraftPacket { this.dimension = dimension; } + public long getPartialHashedSeed() { + return partialHashedSeed; + } + public short getDifficulty() { return difficulty; } @@ -91,6 +97,7 @@ public class JoinGame implements MinecraftPacket { + "entityId=" + entityId + ", gamemode=" + gamemode + ", dimension=" + dimension + + ", partialHashedSeed=" + partialHashedSeed + ", difficulty=" + difficulty + ", maxPlayers=" + maxPlayers + ", levelType='" + levelType + '\'' @@ -111,6 +118,9 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) { this.difficulty = buf.readUnsignedByte(); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { + this.partialHashedSeed = buf.readLong(); + } this.maxPlayers = buf.readUnsignedByte(); this.levelType = ProtocolUtils.readString(buf, 16); if (version.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) { @@ -119,6 +129,9 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { this.reducedDebugInfo = buf.readBoolean(); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { + this.mystery = buf.readBoolean(); + } } @Override @@ -133,6 +146,9 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) { buf.writeByte(difficulty); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { + buf.writeLong(partialHashedSeed); + } buf.writeByte(maxPlayers); if (levelType == null) { throw new IllegalStateException("No level type specified."); @@ -144,6 +160,9 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { buf.writeBoolean(reducedDebugInfo); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { + buf.writeBoolean(mystery); + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java index 4979954fe..847a722c0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java @@ -9,6 +9,7 @@ import io.netty.buffer.ByteBuf; public class Respawn implements MinecraftPacket { private int dimension; + private long partialHashedSeed; private short difficulty; private short gamemode; private String levelType = ""; @@ -16,8 +17,10 @@ public class Respawn implements MinecraftPacket { public Respawn() { } - public Respawn(int dimension, short difficulty, short gamemode, String levelType) { + public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode, + String levelType) { this.dimension = dimension; + this.partialHashedSeed = partialHashedSeed; this.difficulty = difficulty; this.gamemode = gamemode; this.levelType = levelType; @@ -31,6 +34,14 @@ public class Respawn implements MinecraftPacket { this.dimension = dimension; } + public long getPartialHashedSeed() { + return partialHashedSeed; + } + + public void setPartialHashedSeed(long partialHashedSeed) { + this.partialHashedSeed = partialHashedSeed; + } + public short getDifficulty() { return difficulty; } @@ -59,6 +70,7 @@ public class Respawn implements MinecraftPacket { public String toString() { return "Respawn{" + "dimension=" + dimension + + ", partialHashedSeed=" + partialHashedSeed + ", difficulty=" + difficulty + ", gamemode=" + gamemode + ", levelType='" + levelType + '\'' @@ -71,6 +83,9 @@ public class Respawn implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) { this.difficulty = buf.readUnsignedByte(); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { + this.partialHashedSeed = buf.readLong(); + } this.gamemode = buf.readUnsignedByte(); this.levelType = ProtocolUtils.readString(buf, 16); } @@ -81,6 +96,9 @@ public class Respawn implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) { buf.writeByte(difficulty); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { + buf.writeLong(partialHashedSeed); + } buf.writeByte(gamemode); ProtocolUtils.writeString(buf, levelType); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java index 87fb83524..aaa0a8643 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java @@ -17,7 +17,7 @@ public class VelocityNettyThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { - String name = String.format(nameFormat, threadNumber.incrementAndGet()); + String name = String.format(nameFormat, threadNumber.getAndIncrement()); return new FastThreadLocalThread(r, name); } }