3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-11-17 05:20:14 +01:00

Merge branch 'dev/1.1.0' into bungeequack-integrate

Dieser Commit ist enthalten in:
Andrew Steinborn 2019-11-26 15:52:53 -05:00
Commit b08f27b5c4
47 geänderte Dateien mit 901 neuen und 377 gelöschten Zeilen

4
.gitignore vendored
Datei anzeigen

@ -87,5 +87,5 @@ run/
plugins/
### Natives stuff ###
natives/mbedtls
natives/zlib-ng
native/mbedtls
native/zlib-ng

Datei anzeigen

@ -8,3 +8,4 @@ cache:
- $HOME/.gradle/wrapper/
jdk:
- openjdk8
- openjdk11

Datei anzeigen

@ -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 {

Datei anzeigen

@ -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);
}
}

Datei anzeigen

@ -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<RegisteredServer> 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
+ '}';
}
}

Datei anzeigen

@ -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<RegisteredServer> getPreviousServer() {
return Optional.ofNullable(previousServer);
}
@Override
public String toString() {
return "ServerConnectedEvent{"
+ "player=" + player
+ ", server=" + server
+ ", previousServer=" + previousServer
+ '}';
}
}

Datei anzeigen

@ -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;

Datei anzeigen

@ -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];
}
}

Datei anzeigen

@ -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);
}
/**

Datei anzeigen

@ -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'

Datei anzeigen

@ -1,5 +1,7 @@
package com.velocitypowered.natives;
import com.velocitypowered.natives.util.BufferPreference;
public interface Native {
boolean isNative();
BufferPreference preferredBufferType();
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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
}

Datei anzeigen

@ -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");
}
}
}

Datei anzeigen

@ -15,7 +15,7 @@ public final class NativeCodeLoader<T> implements Supplier<T> {
@Override
public T get() {
return selected.object;
return selected.constructed;
}
private static <T> Variant<T> getVariant(List<Variant<T>> variants) {
@ -38,9 +38,14 @@ public final class NativeCodeLoader<T> implements Supplier<T> {
private Status status;
private final Runnable setup;
private final String name;
private final T object;
private final Supplier<T> 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<T> object) {
this.status =
possiblyAvailable.getAsBoolean() ? Status.POSSIBLY_AVAILABLE : Status.NOT_AVAILABLE;
this.setup = setup;
@ -57,6 +62,7 @@ public final class NativeCodeLoader<T> implements Supplier<T> {
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<T> implements Supplier<T> {
}
}
return object;
return constructed;
}
}

Datei anzeigen

@ -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;
};
}

Datei anzeigen

@ -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)
)

Datei anzeigen

@ -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<ByteBuf> bufSupplier)
throws DataFormatException {
ByteBuf source = bufSupplier.get();

Datei anzeigen

@ -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();
}

Datei anzeigen

@ -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'

Datei anzeigen

@ -72,7 +72,10 @@ public class VelocityCommand implements Command {
@Override
public List<String> 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));

Datei anzeigen

@ -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<String, String> 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<String, String> 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<String, List<String>> 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<String, List<String>> 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 {

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;

Datei anzeigen

@ -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<UUID> 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<Offer> 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;
}

Datei anzeigen

@ -425,33 +425,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return;
}
if (connectedServer == null) {
Optional<RegisteredServer> 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<RegisteredServer> next = getNextServerToTry(rs);
result = next.<ServerKickResult>map(RedirectPlayer::create)
.orElseGet(() -> DisconnectPlayer.create(friendlyReason));
} else {
boolean kickedFromCurrent = connectedServer.getServer().equals(rs);
ServerKickResult result;
if (kickedFromCurrent) {
Optional<RegisteredServer> next = getNextServerToTry(rs);
result = next.<ServerKickResult>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) {

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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<RegisteredServer> 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<RegisteredServer> initialFromConfig = player.getNextServerToTry();
PlayerChooseInitialServerEvent event = new PlayerChooseInitialServerEvent(player,
initialFromConfig.orElse(null));
server.getEventManager().fire(event)
.thenRunAsync(() -> {
Optional<RegisteredServer> 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();

Datei anzeigen

@ -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.
*/

Datei anzeigen

@ -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<? extends ServerSocketChannel> serverSocketChannelClass;
@ -62,6 +71,8 @@ enum TransportType {
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}

Datei anzeigen

@ -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("<UNKNOWN>"), Joiner.on(", ").join(plugin.getAuthors()));
registerPlugin(pluginObject);
}
}

Datei anzeigen

@ -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 {

Datei anzeigen

@ -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<DatagramPacket>
private final Cache<InetAddress, Integer> sessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
private final SecureRandom random;
private volatile @MonotonicNonNull List<QueryResponse.PluginInformation> pluginInformationList
= null;
@ -67,6 +69,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket>
public GS4QueryHandler(VelocityServer server) {
this.server = server;
this.random = new SecureRandom();
}
private QueryResponse createInitialResponse() {
@ -111,7 +114,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket>
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

Datei anzeigen

@ -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<ByteBuf> {
private final VelocityCipher cipher;
@ -18,16 +18,19 @@ public class MinecraftCipherDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> 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();
}
}

Datei anzeigen

@ -20,9 +20,11 @@ public class MinecraftCipherEncoder extends MessageToMessageEncoder<ByteBuf> {
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> 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;
}
}

Datei anzeigen

@ -13,7 +13,8 @@ import java.util.List;
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
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<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> 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();

Datei anzeigen

@ -47,20 +47,21 @@ public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
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);
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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<ByteBuf> {
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<ByteBu
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> 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());

Datei anzeigen

@ -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

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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);
}
}