diff --git a/api/build.gradle b/api/build.gradle index d18989984..fd0e92bac 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -89,19 +89,4 @@ publishing { artifact javadocJar } } - - repositories { - maven { - credentials { - username System.getenv("NEXUS_USERNAME") - password System.getenv("NEXUS_PASSWORD") - } - - name = 'velocity-nexus' - def base = 'https://nexus.velocitypowered.com/repository/velocity-artifacts' - def releasesRepoUrl = "$base-releases/" - def snapshotsRepoUrl = "$base-snapshots/" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl - } - } } 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 830911c0e..ce5130078 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -41,7 +41,7 @@ public enum ProtocolVersion { MINECRAFT_1_16_1(736, "1.16.1"), MINECRAFT_1_16_2(751, "1.16.2"), MINECRAFT_1_16_3(753, "1.16.3"), - MINECRAFT_1_16_4(754, 2, "1.16.4"); + MINECRAFT_1_16_4(754, "1.16.4"); private static final int SNAPSHOT_BIT = 30; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java index b63b6b1ac..01a582035 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java @@ -4,6 +4,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.util.Objects; import java.util.regex.Pattern; +import net.kyori.minecraft.Key; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -50,6 +51,35 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { return new MinecraftChannelIdentifier(namespace, name); } + /** + * Creates an channel identifier from the specified Minecraft identifier. + * + * @param identifier the Minecraft identifier + * @return a new channel identifier + */ + public static MinecraftChannelIdentifier from(String identifier) { + int colonPos = identifier.indexOf(':'); + if (colonPos == -1) { + throw new IllegalArgumentException("Identifier does not contain a colon."); + } + if (colonPos + 1 == identifier.length()) { + throw new IllegalArgumentException("Identifier is empty."); + } + String namespace = identifier.substring(0, colonPos); + String name = identifier.substring(colonPos + 1); + return create(namespace, name); + } + + /** + * Creates an channel identifier from the specified Minecraft identifier. + * + * @param key the Minecraft key to use + * @return a new channel identifier + */ + public static MinecraftChannelIdentifier from(Key key) { + return create(key.namespace(), key.value()); + } + public String getNamespace() { return namespace; } @@ -58,6 +88,10 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { return name; } + public Key asKey() { + return Key.of(namespace, name); + } + @Override public String toString() { return namespace + ":" + name + " (modern)"; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java index df7bbe29b..d7f840ee2 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java @@ -104,8 +104,9 @@ public final class ServerPing { /** * Returns a copy of this {@link ServerPing} instance as a builder so that it can be modified. - * It is guaranteed that {@code ping.asBuilder().ping().equals(ping)}: that is, if no other - * changes are made to the returned builder, the built instance will equal the original instance. + * It is guaranteed that {@code ping.asBuilder().build().equals(ping)} is true: that is, if no + * other changes are made to the returned builder, the built instance will equal the original + * instance. * * @return a copy of this instance as a {@link Builder} */ diff --git a/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java b/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java index 00699ba5e..dfa738198 100644 --- a/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java +++ b/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java @@ -1,7 +1,9 @@ package com.velocitypowered.api.proxy.messages; import static com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier.create; +import static com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier.from; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -26,4 +28,24 @@ class MinecraftChannelIdentifierTest { () -> assertThrows(IllegalArgumentException.class, () -> create("minecraft", null)) ); } + + @Test + void fromIdentifierIsCorrect() { + MinecraftChannelIdentifier expected = MinecraftChannelIdentifier.create("velocity", "test"); + assertEquals(expected, MinecraftChannelIdentifier.from("velocity:test")); + } + + @Test + void fromIdentifierThrowsOnBadValues() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> from("")), + () -> assertThrows(IllegalArgumentException.class, () -> from(":")), + () -> assertThrows(IllegalArgumentException.class, () -> from(":a")), + () -> assertThrows(IllegalArgumentException.class, () -> from("a:")), + () -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")), + () -> assertThrows(IllegalArgumentException.class, () -> from("hello::")) + ); + } + + } \ No newline at end of file diff --git a/gradle/publish.gradle b/gradle/publish.gradle index cb94e0aa1..7c79945e9 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -8,7 +8,7 @@ publishing { name = 'velocity-nexus' def base = 'https://nexus.velocitypowered.com/repository/velocity-artifacts' - def releasesRepoUrl = "$base-releases/" + def releasesRepoUrl = "$base-release/" def snapshotsRepoUrl = "$base-snapshots/" url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl } diff --git a/native/compile-linux.sh b/native/compile-linux.sh index 49c621af9..ceaa58251 100755 --- a/native/compile-linux.sh +++ b/native/compile-linux.sh @@ -1,5 +1,11 @@ #!/bin/bash +if [ ! "$CC" ]; then + # The libdeflate authors recommend that we build using GCC as it produces "slightly faster binaries": + # https://github.com/ebiggers/libdeflate#for-unix + export CC=gcc +fi + if [ ! -d libdeflate ]; then echo "Cloning libdeflate..." git clone https://github.com/ebiggers/libdeflate.git @@ -10,10 +16,10 @@ cd libdeflate || exit CFLAGS="-fPIC -O2 -fomit-frame-pointer" make cd .. -CFLAGS="-O2 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -Wl,-z,noexecstack -fomit-frame-pointer" +CFLAGS="-O2 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -Wl,-z,noexecstack -Wall -Werror -fomit-frame-pointer" ARCH=$(uname -m) mkdir -p src/main/resources/linux_$ARCH -gcc $CFLAGS -Ilibdeflate src/main/c/jni_util.c src/main/c/jni_zlib_deflate.c src/main/c/jni_zlib_inflate.c \ +$CC $CFLAGS -Ilibdeflate src/main/c/jni_util.c src/main/c/jni_zlib_deflate.c src/main/c/jni_zlib_inflate.c \ libdeflate/libdeflate.a -o src/main/resources/linux_$ARCH/velocity-compress.so -gcc $CFLAGS -I $MBEDTLS_ROOT/include -shared src/main/c/jni_util.c src/main/c/jni_cipher.c \ +$CC $CFLAGS -shared src/main/c/jni_util.c src/main/c/jni_cipher.c \ -o src/main/resources/linux_$ARCH/velocity-cipher.so -lcrypto \ No newline at end of file diff --git a/native/src/main/c/jni_cipher.c b/native/src/main/c/jni_cipher.c index e0b0b84e8..83515be52 100644 --- a/native/src/main/c/jni_cipher.c +++ b/native/src/main/c/jni_cipher.c @@ -8,29 +8,32 @@ typedef unsigned char byte; JNIEXPORT jlong JNICALL Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_init(JNIEnv *env, - jobject obj, + jclass clazz, jbyteArray key, jboolean encrypt) { + jsize keyLen = (*env)->GetArrayLength(env, key); + if (keyLen != 16) { + throwException(env, "java/lang/IllegalArgumentException", "cipher not 16 bytes"); + return 0; + } + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if (ctx == NULL) { throwException(env, "java/lang/OutOfMemoryError", "allocate cipher"); return 0; } - jsize keyLen = (*env)->GetArrayLength(env, key); - jbyte* keyBytes = (*env)->GetPrimitiveArrayCritical(env, key, NULL); - if (keyBytes == NULL) { - EVP_CIPHER_CTX_free(ctx); - throwException(env, "java/lang/OutOfMemoryError", "cipher get key"); + // Since we know the array size is always bounded, we can just use GetArrayRegion + // and save ourselves some error-checking headaches. + jbyte keyBytes[16]; + (*env)->GetByteArrayRegion(env, key, 0, keyLen, (jbyte*) keyBytes); + if ((*env)->ExceptionCheck(env)) { return 0; } int result = EVP_CipherInit(ctx, EVP_aes_128_cfb8(), (byte*) keyBytes, (byte*) keyBytes, encrypt); - // Release the key byte array now - we won't need it - (*env)->ReleasePrimitiveArrayCritical(env, key, keyBytes, 0); - if (result != 1) { EVP_CIPHER_CTX_free(ctx); throwException(env, "java/security/GeneralSecurityException", "openssl initialize cipher"); @@ -41,7 +44,7 @@ Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_init(JNIEnv *env, JNIEXPORT void JNICALL Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_free(JNIEnv *env, - jobject obj, + jclass clazz, jlong ptr) { EVP_CIPHER_CTX_free((EVP_CIPHER_CTX *) ptr); @@ -49,7 +52,7 @@ Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_free(JNIEnv *env, JNIEXPORT void JNICALL Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_process(JNIEnv *env, - jobject obj, + jclass clazz, jlong ptr, jlong source, jint len, diff --git a/native/src/main/c/jni_zlib_deflate.c b/native/src/main/c/jni_zlib_deflate.c index 7a84a5b04..8dcd60f09 100644 --- a/native/src/main/c/jni_zlib_deflate.c +++ b/native/src/main/c/jni_zlib_deflate.c @@ -7,7 +7,7 @@ JNIEXPORT jlong JNICALL Java_com_velocitypowered_natives_compression_NativeZlibDeflate_init(JNIEnv *env, - jobject obj, + jclass clazz, jint level) { struct libdeflate_compressor *compressor = libdeflate_alloc_compressor(level); @@ -21,7 +21,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibDeflate_init(JNIEnv *env, JNIEXPORT void JNICALL Java_com_velocitypowered_natives_compression_NativeZlibDeflate_free(JNIEnv *env, - jobject obj, + jclass clazz, jlong ctx) { libdeflate_free_compressor((struct libdeflate_compressor *) ctx); @@ -29,13 +29,12 @@ Java_com_velocitypowered_natives_compression_NativeZlibDeflate_free(JNIEnv *env, JNIEXPORT jboolean JNICALL Java_com_velocitypowered_natives_compression_NativeZlibDeflate_process(JNIEnv *env, - jobject obj, + jclass clazz, jlong ctx, jlong sourceAddress, jint sourceLength, jlong destinationAddress, - jint destinationLength, - jboolean finish) + jint destinationLength) { struct libdeflate_compressor *compressor = (struct libdeflate_compressor *) ctx; size_t produced = libdeflate_zlib_compress(compressor, (void *) sourceAddress, sourceLength, diff --git a/native/src/main/c/jni_zlib_inflate.c b/native/src/main/c/jni_zlib_inflate.c index 9a81f0db8..d91319089 100644 --- a/native/src/main/c/jni_zlib_inflate.c +++ b/native/src/main/c/jni_zlib_inflate.c @@ -7,7 +7,7 @@ JNIEXPORT jlong JNICALL Java_com_velocitypowered_natives_compression_NativeZlibInflate_init(JNIEnv *env, - jobject obj) + jclass clazz) { struct libdeflate_decompressor *decompress = libdeflate_alloc_decompressor(); if (decompress == NULL) { @@ -21,7 +21,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_init(JNIEnv *env, JNIEXPORT void JNICALL Java_com_velocitypowered_natives_compression_NativeZlibInflate_free(JNIEnv *env, - jobject obj, + jclass clazz, jlong ctx) { libdeflate_free_decompressor((struct libdeflate_decompressor *) ctx); @@ -29,7 +29,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_free(JNIEnv *env, JNIEXPORT jboolean JNICALL Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *env, - jobject obj, + jclass clazz, jlong ctx, jlong sourceAddress, jint sourceLength, @@ -53,5 +53,9 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e // These cases are the same for us. We expect the full uncompressed size to be known. throwException(env, "java/util/zip/DataFormatException", "uncompressed size is inaccurate"); return JNI_FALSE; + default: + // Unhandled case + throwException(env, "java/util/zip/DataFormatException", "unknown libdeflate return code"); + return JNI_FALSE; } } \ No newline at end of file diff --git a/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java index 19dead4ac..ac48b357b 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java @@ -9,9 +9,7 @@ public class LibdeflateVelocityCompressor implements VelocityCompressor { public static final VelocityCompressorFactory FACTORY = LibdeflateVelocityCompressor::new; - private final NativeZlibInflate inflate = new NativeZlibInflate(); private final long inflateCtx; - private final NativeZlibDeflate deflate = new NativeZlibDeflate(); private final long deflateCtx; private boolean disposed = false; @@ -21,8 +19,8 @@ public class LibdeflateVelocityCompressor implements VelocityCompressor { throw new IllegalArgumentException("Invalid compression level " + level); } - this.inflateCtx = inflate.init(); - this.deflateCtx = deflate.init(correctedLevel); + this.inflateCtx = NativeZlibInflate.init(); + this.deflateCtx = NativeZlibDeflate.init(correctedLevel); } @Override @@ -38,7 +36,7 @@ public class LibdeflateVelocityCompressor implements VelocityCompressor { long sourceAddress = source.memoryAddress() + source.readerIndex(); long destinationAddress = destination.memoryAddress() + destination.writerIndex(); - inflate.process(inflateCtx, sourceAddress, source.readableBytes(), destinationAddress, + NativeZlibInflate.process(inflateCtx, sourceAddress, source.readableBytes(), destinationAddress, uncompressedSize); destination.writerIndex(destination.writerIndex() + uncompressedSize); } @@ -51,7 +49,7 @@ public class LibdeflateVelocityCompressor implements VelocityCompressor { long sourceAddress = source.memoryAddress() + source.readerIndex(); long destinationAddress = destination.memoryAddress() + destination.writerIndex(); - int produced = deflate.process(deflateCtx, sourceAddress, source.readableBytes(), + int produced = NativeZlibDeflate.process(deflateCtx, sourceAddress, source.readableBytes(), destinationAddress, destination.writableBytes()); if (produced > 0) { destination.writerIndex(destination.writerIndex() + produced); @@ -70,8 +68,8 @@ public class LibdeflateVelocityCompressor implements VelocityCompressor { @Override public void close() { if (!disposed) { - inflate.free(inflateCtx); - deflate.free(deflateCtx); + NativeZlibInflate.free(inflateCtx); + NativeZlibDeflate.free(deflateCtx); } disposed = true; } diff --git a/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibDeflate.java b/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibDeflate.java index eb89412cb..83cdc5cd1 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibDeflate.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibDeflate.java @@ -5,10 +5,10 @@ package com.velocitypowered.natives.compression; */ class NativeZlibDeflate { - native long init(int level); + static native long init(int level); - native long free(long ctx); + static native long free(long ctx); - native int process(long ctx, long sourceAddress, int sourceLength, long destinationAddress, + static native int process(long ctx, long sourceAddress, int sourceLength, long destinationAddress, int destinationLength); } diff --git a/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibInflate.java b/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibInflate.java index fc6e9787f..1c6c42594 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibInflate.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/NativeZlibInflate.java @@ -7,10 +7,10 @@ import java.util.zip.DataFormatException; */ class NativeZlibInflate { - native long init(); + static native long init(); - native long free(long ctx); + static native long free(long ctx); - native boolean process(long ctx, long sourceAddress, int sourceLength, long destinationAddress, - int destinationLength) throws DataFormatException; + static native boolean process(long ctx, long sourceAddress, int sourceLength, + long destinationAddress, int destinationLength) throws DataFormatException; } 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 bee59ac7a..814971c4e 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java @@ -19,13 +19,11 @@ public class NativeVelocityCipher implements VelocityCipher { return new NativeVelocityCipher(false, key); } }; - private static final OpenSslCipherImpl impl = new OpenSslCipherImpl(); - private final long ctx; private boolean disposed = false; private NativeVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException { - this.ctx = impl.init(key.getEncoded(), encrypt); + this.ctx = OpenSslCipherImpl.init(key.getEncoded(), encrypt); } @Override @@ -35,13 +33,13 @@ public class NativeVelocityCipher implements VelocityCipher { long base = source.memoryAddress() + source.readerIndex(); int len = source.readableBytes(); - impl.process(ctx, base, len, base); + OpenSslCipherImpl.process(ctx, base, len, base); } @Override public void close() { if (!disposed) { - impl.free(ctx); + OpenSslCipherImpl.free(ctx); } disposed = true; } diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/OpenSslCipherImpl.java b/native/src/main/java/com/velocitypowered/natives/encryption/OpenSslCipherImpl.java index da5fc6b50..f97eef3df 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/OpenSslCipherImpl.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/OpenSslCipherImpl.java @@ -4,9 +4,9 @@ import java.security.GeneralSecurityException; class OpenSslCipherImpl { - native long init(byte[] key, boolean encrypt) throws GeneralSecurityException; + static native long init(byte[] key, boolean encrypt) throws GeneralSecurityException; - native void process(long ctx, long source, int len, long dest); + static native void process(long ctx, long source, int len, long dest); - native void free(long ptr); + static native void free(long ptr); } diff --git a/native/src/main/resources/linux_aarch64/velocity-cipher.so b/native/src/main/resources/linux_aarch64/velocity-cipher.so index ba4d9644b..155d8db6c 100755 Binary files a/native/src/main/resources/linux_aarch64/velocity-cipher.so and b/native/src/main/resources/linux_aarch64/velocity-cipher.so differ diff --git a/native/src/main/resources/linux_aarch64/velocity-compress.so b/native/src/main/resources/linux_aarch64/velocity-compress.so index 2ecbffd66..52ddff982 100755 Binary files a/native/src/main/resources/linux_aarch64/velocity-compress.so and b/native/src/main/resources/linux_aarch64/velocity-compress.so differ diff --git a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl10x.so b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl10x.so index f69edf145..828c5757b 100755 Binary files a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl10x.so and b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl10x.so differ diff --git a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl11x.so b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl11x.so index 37451294a..179b1c9e6 100755 Binary files a/native/src/main/resources/linux_x86_64/velocity-cipher-ossl11x.so and b/native/src/main/resources/linux_x86_64/velocity-cipher-ossl11x.so differ diff --git a/native/src/main/resources/linux_x86_64/velocity-compress.so b/native/src/main/resources/linux_x86_64/velocity-compress.so index 29b48a8f1..123a838b1 100755 Binary files a/native/src/main/resources/linux_x86_64/velocity-compress.so and b/native/src/main/resources/linux_x86_64/velocity-compress.so differ 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 0e76bdd0a..7fe7e777b 100644 --- a/native/src/test/java/com/velocitypowered/natives/encryption/VelocityCipherTest.java +++ b/native/src/test/java/com/velocitypowered/natives/encryption/VelocityCipherTest.java @@ -2,6 +2,7 @@ package com.velocitypowered.natives.encryption; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.condition.OS.LINUX; import com.velocitypowered.natives.util.Natives; import io.netty.buffer.ByteBuf; @@ -13,6 +14,7 @@ import java.util.function.Supplier; import javax.crypto.spec.SecretKeySpec; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; class VelocityCipherTest { @@ -29,6 +31,7 @@ class VelocityCipherTest { } @Test + @EnabledOnOs({LINUX}) void nativeIntegrityCheck() throws GeneralSecurityException { VelocityCipherFactory factory = Natives.cipher.get(); if (factory == JavaVelocityCipher.FACTORY) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 1be7f950c..e2c6670d7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -415,8 +415,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { .toArray((IntFunction[]>) CompletableFuture[]::new)); playersTeardownFuture.get(10, TimeUnit.SECONDS); - } catch (TimeoutException | ExecutionException e) { + } catch (TimeoutException e) { timedOut = true; + } catch (ExecutionException e) { + timedOut = true; + logger.error("Exception while tearing down player connections", e); } eventManager.fireShutdownEvent(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 02c085241..3218960b9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -3,19 +3,35 @@ package com.velocitypowered.proxy.command.builtin; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; + +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.SimpleCommand; import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.util.InformationUtils; + +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; + import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; @@ -25,6 +41,10 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.BoundRequestBuilder; +import org.asynchttpclient.ListenableFuture; +import org.asynchttpclient.Response; import org.checkerframework.checker.nullness.qual.NonNull; public class VelocityCommand implements SimpleCommand { @@ -52,6 +72,7 @@ public class VelocityCommand implements SimpleCommand { .put("version", new Info(server)) .put("plugins", new Plugins(server)) .put("reload", new Reload(server)) + .put("dump", new Dump(server)) .build(); } @@ -289,4 +310,140 @@ public class VelocityCommand implements SimpleCommand { return source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE; } } + + private static class Dump implements SubCommand { + + private static final Logger logger = LogManager.getLogger(Dump.class); + private final ProxyServer server; + + private Dump(ProxyServer server) { + this.server = server; + } + + @Override + public void execute(CommandSource source, String @NonNull [] args) { + if (args.length != 0) { + source.sendMessage(Identity.nil(), Component.text("/velocity dump", NamedTextColor.RED)); + return; + } + + Collection allServers = ImmutableSet.copyOf(server.getAllServers()); + JsonObject servers = new JsonObject(); + for (RegisteredServer iter : allServers) { + servers.add(iter.getServerInfo().getName(), + InformationUtils.collectServerInfo(iter)); + } + JsonArray connectOrder = new JsonArray(); + List attemptedConnectionOrder = ImmutableList.copyOf( + server.getConfiguration().getAttemptConnectionOrder()); + for (int i = 0; i < attemptedConnectionOrder.size(); i++) { + connectOrder.add(attemptedConnectionOrder.get(i)); + } + + JsonObject proxyConfig = InformationUtils.collectProxyConfig(server.getConfiguration()); + proxyConfig.add("servers", servers); + proxyConfig.add("connectOrder", connectOrder); + proxyConfig.add("forcedHosts", + InformationUtils.collectForcedHosts(server.getConfiguration())); + + JsonObject dump = new JsonObject(); + dump.add("versionInfo", InformationUtils.collectProxyInfo(server.getVersion())); + dump.add("platform", InformationUtils.collectEnvironmentInfo()); + dump.add("config", proxyConfig); + dump.add("plugins", InformationUtils.collectPluginInfo(server)); + + source.sendMessage(Component.text().content("Uploading gathered information...").build()); + AsyncHttpClient httpClient = ((VelocityServer) server).getAsyncHttpClient(); + + BoundRequestBuilder request = + httpClient.preparePost("https://dump.velocitypowered.com/documents"); + request.setHeader("Content-Type", "text/plain"); + request.addHeader("User-Agent", server.getVersion().getName() + "/" + + server.getVersion().getVersion()); + request.setBody( + InformationUtils.toHumanReadableString(dump).getBytes(StandardCharsets.UTF_8)); + + ListenableFuture future = request.execute(); + future.addListener(() -> { + try { + Response response = future.get(); + if (response.getStatusCode() != 200) { + source.sendMessage(Component.text() + .content("An error occurred while communicating with the Velocity servers. " + + "The servers may be temporarily unavailable or there is an issue " + + "with your network settings. You can find more information in the " + + "log or console of your Velocity server.") + .color(NamedTextColor.RED).build()); + logger.error("Invalid status code while POST-ing Velocity dump: " + + response.getStatusCode()); + logger.error("Headers: \n--------------BEGIN HEADERS--------------\n" + + response.getHeaders().toString() + + "\n---------------END HEADERS---------------"); + return; + } + JsonObject key = InformationUtils.parseString( + response.getResponseBody(StandardCharsets.UTF_8)); + if (!key.has("key")) { + throw new JsonSyntaxException("Missing Dump-Url-response"); + } + String url = "https://dump.velocitypowered.com/" + + key.get("key").getAsString() + ".json"; + source.sendMessage(Component.text() + .content("Created an anonymised report containing useful information about " + + "this proxy. If a developer requested it, you may share the " + + "following link with them:") + .append(Component.newline()) + .append(Component.text(">> " + url) + .color(NamedTextColor.GREEN) + .clickEvent(ClickEvent.openUrl(url))) + .append(Component.newline()) + .append(Component.text("Note: This link is only valid for a few days") + .color(NamedTextColor.GRAY) + ).build()); + } catch (InterruptedException e) { + source.sendMessage(Component.text() + .content("Could not complete the request, the command was interrupted." + + "Please refer to the proxy-log or console for more information.") + .color(NamedTextColor.RED).build()); + logger.error("Failed to complete dump command, " + + "the executor was interrupted: " + e.getMessage()); + e.printStackTrace(); + } catch (ExecutionException e) { + TextComponent.Builder message = Component.text() + .content("An error occurred while attempting to upload the gathered " + + "information to the Velocity servers.") + .append(Component.newline()) + .color(NamedTextColor.RED); + if (e.getCause() instanceof UnknownHostException + || e.getCause() instanceof ConnectException) { + message.append(Component.text( + "Likely cause: Invalid system DNS settings or no internet connection")); + } + source.sendMessage(message + .append(Component.newline() + .append(Component.text( + "Error details can be found in the proxy log / console")) + ).build()); + + logger.error("Failed to complete dump command, " + + "the executor encountered an Exception: " + e.getCause().getMessage()); + e.getCause().printStackTrace(); + } catch (JsonParseException e) { + source.sendMessage(Component.text() + .content("An error occurred on the Velocity-servers and the dump could not " + + "be completed. Please contact the Velocity staff about this problem. " + + "If you do, provide the details about this error from the Velocity " + + "console or server log.") + .color(NamedTextColor.RED).build()); + logger.error("Invalid response from the Velocity servers: " + e.getMessage()); + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { + return source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE; + } + } } 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 6fcd74545..12ef585d5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -7,6 +7,7 @@ import com.electronwill.nightconfig.toml.TomlFormat; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gson.annotations.Expose; import com.velocitypowered.api.proxy.config.ProxyConfig; import com.velocitypowered.api.util.Favicon; import com.velocitypowered.proxy.util.AddressUtil; @@ -42,20 +43,20 @@ public class VelocityConfiguration implements ProxyConfig { private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class); - private String bind = "0.0.0.0:25577"; - private String motd = "&3A Velocity Server"; - private int showMaxPlayers = 500; - private boolean onlineMode = true; - private boolean preventClientProxyConnections = false; - private PlayerInfoForwarding playerInfoForwardingMode = PlayerInfoForwarding.NONE; + @Expose private String bind = "0.0.0.0:25577"; + @Expose private String motd = "&3A Velocity Server"; + @Expose private int showMaxPlayers = 500; + @Expose private boolean onlineMode = true; + @Expose private boolean preventClientProxyConnections = false; + @Expose private PlayerInfoForwarding playerInfoForwardingMode = PlayerInfoForwarding.NONE; private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8); - private boolean announceForge = false; - private boolean onlineModeKickExistingPlayers = false; - private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; + @Expose private boolean announceForge = false; + @Expose private boolean onlineModeKickExistingPlayers = false; + @Expose private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; private final Servers servers; private final ForcedHosts forcedHosts; - private final Advanced advanced; - private final Query query; + @Expose private final Advanced advanced; + @Expose private final Query query; private final Metrics metrics; private final Messages messages; private net.kyori.adventure.text.@MonotonicNonNull Component motdAsComponent; @@ -612,18 +613,18 @@ public class VelocityConfiguration implements ProxyConfig { private static class Advanced { - private int compressionThreshold = 256; - private int compressionLevel = -1; - private int loginRatelimit = 3000; - private int connectionTimeout = 5000; - private int readTimeout = 30000; - private boolean proxyProtocol = false; - private boolean tcpFastOpen = false; - private boolean bungeePluginMessageChannel = true; - private boolean showPingRequests = false; - private boolean failoverOnUnexpectedServerDisconnect = true; - private boolean announceProxyCommands = true; - private boolean logCommandExecutions = false; + @Expose private int compressionThreshold = 256; + @Expose private int compressionLevel = -1; + @Expose private int loginRatelimit = 3000; + @Expose private int connectionTimeout = 5000; + @Expose private int readTimeout = 30000; + @Expose private boolean proxyProtocol = false; + @Expose private boolean tcpFastOpen = false; + @Expose private boolean bungeePluginMessageChannel = true; + @Expose private boolean showPingRequests = false; + @Expose private boolean failoverOnUnexpectedServerDisconnect = true; + @Expose private boolean announceProxyCommands = true; + @Expose private boolean logCommandExecutions = false; private Advanced() { } @@ -715,10 +716,10 @@ public class VelocityConfiguration implements ProxyConfig { private static class Query { - private boolean queryEnabled = false; - private int queryPort = 25577; - private String queryMap = "Velocity"; - private boolean showPlugins = false; + @Expose private boolean queryEnabled = false; + @Expose private int queryPort = 25577; + @Expose private String queryMap = "Velocity"; + @Expose private boolean showPlugins = false; private Query() { } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index bbba322d9..abd1f7b91 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -149,7 +149,11 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { Unpooled.wrappedBuffer(copy)); playerConnection.write(copied); } - }, playerConnection.eventLoop()); + }, playerConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling plugin message {}", packet, ex); + return null; + }); return true; } @@ -186,7 +190,11 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { server.getEventManager().fire( new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode)) - .thenAcceptAsync(event -> playerConnection.write(commands), playerConnection.eventLoop()); + .thenAcceptAsync(event -> playerConnection.write(commands), playerConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling available commands for {}", playerConnection, ex); + return null; + }); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java index 4bcbf177e..e21b9d2f1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -30,7 +30,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @SuppressFBWarnings(value = "OS_OPEN_STREAM", justification = "Most methods in this class open " + "instances of ByteBufDataOutput backed by heap-allocated ByteBufs. Closing them does " + "nothing.") -class BungeeCordMessageResponder { +public class BungeeCordMessageResponder { private static final MinecraftChannelIdentifier MODERN_CHANNEL = MinecraftChannelIdentifier .create("bungeecord", "main"); @@ -45,6 +45,11 @@ class BungeeCordMessageResponder { this.player = player; } + public static boolean isBungeeCordMessage(PluginMessage message) { + return MODERN_CHANNEL.getId().equals(message.getChannel()) && !LEGACY_CHANNEL.getId() + .equals(message.getChannel()); + } + private void processConnect(ByteBufDataInput in) { String serverName = in.readUTF(); proxy.getServer(serverName).ifPresent(server -> player.createConnectionRequest(server) @@ -332,8 +337,7 @@ class BungeeCordMessageResponder { return false; } - if (!MODERN_CHANNEL.getId().equals(message.getChannel()) && !LEGACY_CHANNEL.getId() - .equals(message.getChannel())) { + if (!isBungeeCordMessage(message)) { return false; } 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 27d938380..d44e65737 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 @@ -16,6 +16,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases; +import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -157,7 +158,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { smc.write(packet); } } - }, smc.eventLoop()); + }, smc.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling player chat for {}", player, ex); + return null; + }); } return true; } @@ -190,6 +195,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } else if (PluginMessageUtil.isMcBrand(packet)) { backendConn.write(PluginMessageUtil .rewriteMinecraftBrand(packet, server.getVersion(), player.getProtocolVersion())); + } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { + return true; } else { if (serverConn.getPhase() == BackendConnectionPhases.IN_TRANSITION) { // We must bypass the currently-connected server when forwarding Forge packets. @@ -225,7 +232,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { Unpooled.wrappedBuffer(copy)); backendConn.write(message); } - }, backendConn.eventLoop()); + }, backendConn.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling plugin message packet for {}", + player, ex); + return null; + }); } } } @@ -423,7 +435,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { resp.getOffers().addAll(offers); player.getConnection().write(resp); } - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling command tab completion for player {} executing {}", + player, command, ex); + return null; + }); return true; // Sorry, handler; we're just gonna have to lie to you here. } @@ -475,7 +492,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getUsername(), command, e); } - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()) + .exceptionally((ex) -> { + logger.error( + "Exception while finishing command tab completion, with request {} and response {}", + request, response, ex); + return null; + }); } private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) { @@ -490,7 +513,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { response.getOffers().add(new Offer(s)); } player.getConnection().write(response); - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()) + .exceptionally((ex) -> { + logger.error( + "Exception while finishing regular tab completion, with request {} and response{}", + request, response, ex); + return null; + }); } private CompletableFuture processCommandExecuteResult(String originalCommand, 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 46d988399..a2679ec1c 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 @@ -637,7 +637,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } DisconnectEvent event = new DisconnectEvent(this, status); - server.getEventManager().fire(event).thenRun(() -> this.teardownFuture.complete(null)); + server.getEventManager().fire(event).whenComplete((val, ex) -> { + if (ex == null) { + this.teardownFuture.complete(null); + } else { + this.teardownFuture.completeExceptionally(ex); + } + }); } public CompletableFuture getTeardownFuture() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java index 1d1485ef8..188ae3537 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java @@ -1,6 +1,7 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; @@ -25,6 +26,8 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler { player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); } else if (PluginMessageUtil.isUnregister(packet)) { player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); + } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { + return true; } serverConn.ensureConnected().write(packet.retain()); } 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 670c34903..e149074d7 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 @@ -183,7 +183,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } else { initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()), false); } - }, mcConnection.eventLoop()); + }, mcConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception in pre-login stage", ex); + return null; + }); } private EncryptionRequest generateEncryptionRequest() { @@ -202,6 +206,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { server.getConfiguration().getPlayerInfoForwardingMode()); GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(inbound, profile, onlineMode); + final GameProfile finalProfile = profile; server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> { if (mcConnection.isClosed()) { @@ -229,6 +234,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler { completeLoginProtocolPhaseAndInitialize(player); } }, mcConnection.eventLoop()); + }).exceptionally((ex) -> { + logger.error("Exception during connection of {}", finalProfile, ex); + return null; }); } @@ -274,7 +282,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { server.getEventManager().fire(new PostLoginEvent(player)) .thenRun(() -> connectToInitialServer(player)); } - }, mcConnection.eventLoop()); + }, mcConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while completing login initialisation phase for {}", player, ex); + return null; + }); } private void connectToInitialServer(ConnectedPlayer player) { @@ -291,7 +303,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { return; } player.createConnectionRequest(toTry.get()).fireAndForget(); - }, mcConnection.eventLoop()); + }, mcConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index d067a3c88..eefb485dd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -120,6 +120,10 @@ public class StatusSessionHandler implements MinecraftSessionHandler { continue; } + if (response.getDescriptionComponent() == null) { + continue; + } + return new ServerPing( fallback.getVersion(), fallback.getPlayers().orElse(null), @@ -163,7 +167,11 @@ public class StatusSessionHandler implements MinecraftSessionHandler { .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping))) .thenAcceptAsync(event -> connection.closeWith( LegacyDisconnect.fromServerPing(event.getPing(), packet.getVersion())), - connection.eventLoop()); + connection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling legacy ping {}", packet, ex); + return null; + }); return true; } @@ -189,7 +197,11 @@ public class StatusSessionHandler implements MinecraftSessionHandler { .toJson(event.getPing(), json); connection.write(new StatusResponse(json)); }, - connection.eventLoop()); + connection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling status request {}", packet, ex); + return null; + }); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java index b914dfd85..8a7e2b53d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java @@ -113,12 +113,10 @@ public class VelocityEventManager implements EventManager { return CompletableFuture.completedFuture(event); } - CompletableFuture eventFuture = new CompletableFuture<>(); - service.execute(() -> { + return CompletableFuture.supplyAsync(() -> { fireEvent(event); - eventFuture.complete(event); - }); - return eventFuture; + return event; + }, service); } @Override 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 87fbc9435..a59318974 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 @@ -30,6 +30,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; +import org.apache.logging.log4j.LogManager; public class GS4QueryHandler extends SimpleChannelInboundHandler { @@ -163,7 +164,12 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler // Send the response DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender()); ctx.writeAndFlush(responsePacket, ctx.voidPromise()); - }, ctx.channel().eventLoop()); + }, ctx.channel().eventLoop()) + .exceptionally((ex) -> { + LogManager.getLogger(getClass()).error( + "Exception while writing GS4 response for query from {}", senderAddress, ex); + return null; + }); break; } default: diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java new file mode 100644 index 000000000..89f24ab9c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java @@ -0,0 +1,240 @@ +package com.velocitypowered.proxy.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; +import com.velocitypowered.api.plugin.meta.PluginDependency; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.config.ProxyConfig; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.util.ProxyVersion; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; + +public enum InformationUtils { + ; + + /** + * Retrieves a {@link JsonArray} containing basic information about all + * running plugins on the {@link ProxyServer} instance. + * + * @param proxy the proxy instance to retrieve from + * @return {@link JsonArray} containing zero or more {@link JsonObject} + */ + public static JsonArray collectPluginInfo(ProxyServer proxy) { + List allPlugins = ImmutableList.copyOf( + proxy.getPluginManager().getPlugins()); + JsonArray plugins = new JsonArray(); + + for (PluginContainer plugin : allPlugins) { + PluginDescription desc = plugin.getDescription(); + JsonObject current = new JsonObject(); + current.addProperty("id", desc.getId()); + if (desc.getName().isPresent()) { + current.addProperty("name", desc.getName().get()); + } + if (desc.getVersion().isPresent()) { + current.addProperty("version", desc.getVersion().get()); + } + if (!desc.getAuthors().isEmpty()) { + JsonArray authorsArray = new JsonArray(); + for (String author : desc.getAuthors()) { + authorsArray.add(author); + } + current.add("authors", authorsArray); + } + if (desc.getDescription().isPresent()) { + current.addProperty("description", desc.getDescription().get()); + } + if (desc.getUrl().isPresent()) { + current.addProperty("url", desc.getUrl().get()); + } + if (!desc.getDependencies().isEmpty()) { + JsonArray dependencies = new JsonArray(); + for (PluginDependency dependency : desc.getDependencies()) { + dependencies.add(dependency.getId()); + } + current.add("dependencies", dependencies); + } + plugins.add(current); + } + return plugins; + } + + /** + * Creates a {@link JsonObject} containing information about the + * current environment the project is run under. + * + * @return {@link JsonObject} containing environment info + */ + public static JsonObject collectEnvironmentInfo() { + JsonObject envInfo = new JsonObject(); + envInfo.addProperty("operatingSystemType", System.getProperty("os.name")); + envInfo.addProperty("operatingSystemVersion", System.getProperty("os.version")); + envInfo.addProperty("operatingSystemArchitecture", System.getProperty("os.arch")); + envInfo.addProperty("javaVersion", System.getProperty("java.version")); + envInfo.addProperty("javaVendor", System.getProperty("java.vendor")); + return envInfo; + } + + /** + * Creates a {@link JsonObject} containing information about the + * forced hosts of the {@link ProxyConfig} instance. + * + * @return {@link JsonArray} containing forced hosts + */ + public static JsonObject collectForcedHosts(ProxyConfig config) { + JsonObject forcedHosts = new JsonObject(); + Map> allForcedHosts = ImmutableMap.copyOf( + config.getForcedHosts()); + for (Map.Entry> entry : allForcedHosts.entrySet()) { + JsonArray host = new JsonArray(); + for (int i = 0; i < entry.getValue().size(); i++) { + host.add(entry.getValue().get(i)); + } + forcedHosts.add(entry.getKey(), host); + } + return forcedHosts; + } + + /** + * Anonymises or redacts a given {@link InetAddress} + * public address bits. + * + * @param address The address to redact + * @return {@link String} address with public parts redacted + */ + public static String anonymizeInetAddress(InetAddress address) { + if (address instanceof Inet4Address) { + Inet4Address v4 = (Inet4Address) address; + if (v4.isAnyLocalAddress() || v4.isLoopbackAddress() + || v4.isLinkLocalAddress() + || v4.isSiteLocalAddress()) { + return address.getHostAddress(); + } else { + byte[] addr = v4.getAddress(); + return (addr[0] & 0xff) + "." + (addr[1] & 0xff) + ".XXX.XXX"; + } + } else if (address instanceof Inet6Address) { + Inet6Address v6 = (Inet6Address) address; + if (v6.isAnyLocalAddress() || v6.isLoopbackAddress() + || v6.isSiteLocalAddress() + || v6.isSiteLocalAddress()) { + return address.getHostAddress(); + } else { + String[] bits = v6.getHostAddress().split(":"); + String ret = ""; + boolean flag = false; + for (int iter = 0; iter < bits.length; iter++) { + if (flag) { + ret += ":X"; + continue; + } + if (!bits[iter].equals("0")) { + if (iter == 0) { + ret = bits[iter]; + } else { + ret = "::" + bits[iter]; + } + flag = true; + } + } + return ret; + } + } else { + return address.getHostAddress(); + } + } + + /** + * Creates a {@link JsonObject} containing most relevant + * information of the {@link RegisteredServer} for diagnosis. + * + * @param server the server to evaluate + * @return {@link JsonObject} containing server and diagnostic info + */ + public static JsonObject collectServerInfo(RegisteredServer server) { + JsonObject info = new JsonObject(); + info.addProperty("currentPlayers", server.getPlayersConnected().size()); + InetSocketAddress iaddr = server.getServerInfo().getAddress(); + if (iaddr.isUnresolved()) { + // Greetings form Netty 4aa10db9 + info.addProperty("host", iaddr.getHostString()); + } else { + info.addProperty("host", anonymizeInetAddress(iaddr.getAddress())); + } + info.addProperty("port", iaddr.getPort()); + return info; + } + + /** + * Creates a {@link JsonObject} containing information about the + * current environment the project is run under. + * + * @param version the proxy instance to retrieve from + * @return {@link JsonObject} containing environment info + */ + public static JsonObject collectProxyInfo(ProxyVersion version) { + return (JsonObject) serializeObject(version, false); + } + + /** + * Creates a {@link JsonObject} containing most relevant + * information of the {@link ProxyConfig} for diagnosis. + * + * @param config the config instance to retrieve from + * @return {@link JsonObject} containing select config values + */ + public static JsonObject collectProxyConfig(ProxyConfig config) { + return (JsonObject) serializeObject(config, true); + } + + /** + * Creates a human-readable String from a {@link JsonElement}. + * + * @param json the {@link JsonElement} object + * @return the human-readable String + */ + public static String toHumanReadableString(JsonElement json) { + return GSON_WITHOUT_EXCLUDES.toJson(json); + } + + /** + * Creates a {@link JsonObject} from a String. + * + * @param toParse the String to parse + * @return {@link JsonObject} object + */ + public static JsonObject parseString(String toParse) { + return GSON_WITHOUT_EXCLUDES.fromJson(toParse, JsonObject.class); + } + + private static JsonElement serializeObject(Object toSerialize, boolean withExcludes) { + return JsonParser.parseString( + withExcludes ? GSON_WITH_EXCLUDES.toJson(toSerialize) : + GSON_WITHOUT_EXCLUDES.toJson(toSerialize)); + } + + private static final Gson GSON_WITH_EXCLUDES = new GsonBuilder() + .setPrettyPrinting() + .excludeFieldsWithoutExposeAnnotation() + .create(); + + private static final Gson GSON_WITHOUT_EXCLUDES = new GsonBuilder() + .setPrettyPrinting() + .create(); + + +}