diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 000000000..da4514613 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,32 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: [push, pull_request] + +jobs: + build-8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + build-11: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e44a7026d..e6b626f81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,9 +28,7 @@ sure that you are properly adhering to the code style. To reduce bugs and ensure code quality, we run the following tools on all commits and pull requests: -* [Checker Framework](https://checkerframework.org/): an enhancement to Java's type - system that is designed to help catch bugs. Velocity runs the _Nullness Checker_ - and the _Optional Checker_. The build will fail if Checker Framework notices an - issue. +* [SpotBugs](https://spotbugs.github.io/): ensures that common errors do not + get into the codebase. The build will fail if SpotBugs finds an issue. * [Checkstyle](http://checkstyle.sourceforge.net/): ensures that your code is - correctly formatted. The build will fail if Checkstyle detects a problem. \ No newline at end of file + correctly formatted. The build will fail if Checkstyle detects a problem. diff --git a/api/build.gradle b/api/build.gradle index 342cc71b9..6240c4b98 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -81,9 +81,18 @@ 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' + + // Remove 'undefined' from seach paths when generating javadoc for a non-modular project (JDK-8215291) + if (JavaVersion.current() >= JavaVersion.VERSION_1_9 && JavaVersion.current() < JavaVersion.VERSION_12) { + options.addBooleanOption('-no-module-directories', true) + } +} + +test { + useJUnitPlatform() } test { diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChannelRegisterEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChannelRegisterEvent.java new file mode 100644 index 000000000..d9c790eed --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChannelRegisterEvent.java @@ -0,0 +1,38 @@ +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; + +import java.util.List; + +/** + * This event is fired when a client ({@link Player}) sends a plugin message through the + * register channel. + */ +public final class PlayerChannelRegisterEvent { + + private final Player player; + private final List channels; + + public PlayerChannelRegisterEvent(Player player, List channels) { + this.player = Preconditions.checkNotNull(player, "player"); + this.channels = Preconditions.checkNotNull(channels, "channels"); + } + + public Player getPlayer() { + return player; + } + + public List getChannels() { + return channels; + } + + @Override + public String toString() { + return "PlayerChannelRegisterEvent{" + + "player=" + player + + ", channels=" + channels + + '}'; + } +} 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 9f3478232..b374f5b47 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 @@ -12,7 +12,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public final class MinecraftChannelIdentifier implements ChannelIdentifier { - private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]*"); + private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9/\\-_]*"); private final String namespace; private final String name; 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 d7f840ee2..8b2f0b0dd 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 @@ -227,6 +227,11 @@ public final class ServerPing { return this; } + public Builder clearFavicon() { + this.favicon = null; + return this; + } + /** * Uses the information from this builder to create a new {@link ServerPing} instance. The * builder can be re-used after this event has been called. 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 dfa738198..e7f02e95a 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 @@ -35,6 +35,11 @@ class MinecraftChannelIdentifierTest { assertEquals(expected, MinecraftChannelIdentifier.from("velocity:test")); } + @Test + void createAllowsSlashes() { + create("velocity", "test/test2"); + } + @Test void fromIdentifierThrowsOnBadValues() { assertAll( diff --git a/build.gradle b/build.gradle index ad0b8b1f4..5723240f0 100644 --- a/build.gradle +++ b/build.gradle @@ -20,11 +20,11 @@ allprojects { ext { // dependency versions - adventureVersion = '4.1.1' + adventureVersion = '4.5.0' junitVersion = '5.7.0' slf4jVersion = '1.7.30' log4jVersion = '2.13.3' - nettyVersion = '4.1.56.Final' + nettyVersion = '4.1.59.Final' guavaVersion = '30.0-jre' checkerFrameworkVersion = '3.6.1' configurateVersion = '4.0.0-SNAPSHOT' diff --git a/proxy/build.gradle b/proxy/build.gradle index d4a34e027..88fdaabcb 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -55,13 +55,13 @@ dependencies { implementation "io.netty:netty-handler:${nettyVersion}" implementation "io.netty:netty-transport-native-epoll:${nettyVersion}" implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64" - implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch64" - implementation "io.netty:netty-resolver-dns:${nettyVersion}" + implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch_64" implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}" implementation "org.apache.logging.log4j:log4j-core:${log4jVersion}" implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}" implementation "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}" + implementation "org.apache.logging.log4j:log4j-jul:${log4jVersion}" implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options implementation 'net.minecrell:terminalconsoleappender:1.2.0' @@ -76,7 +76,7 @@ dependencies { implementation 'com.spotify:completable-futures:0.3.3' implementation 'com.electronwill.night-config:toml:3.6.3' - + implementation 'org.bstats:bstats-base:2.2.0' implementation 'org.lanternpowered:lmbda:2.0.0-SNAPSHOT' implementation 'com.github.ben-manes.caffeine:caffeine:2.8.8' @@ -117,7 +117,6 @@ shadowJar { exclude 'it/unimi/dsi/fastutil/objects/*Object2Float*' exclude 'it/unimi/dsi/fastutil/objects/*Object2IntArray*' exclude 'it/unimi/dsi/fastutil/objects/*Object2IntAVL*' - exclude 'it/unimi/dsi/fastutil/objects/*Object*OpenCustom*' exclude 'it/unimi/dsi/fastutil/objects/*Object2IntRB*' exclude 'it/unimi/dsi/fastutil/objects/*Object2Long*' exclude 'it/unimi/dsi/fastutil/objects/*Object2Object*' @@ -127,12 +126,10 @@ shadowJar { exclude 'it/unimi/dsi/fastutil/objects/*Reference*' exclude 'it/unimi/dsi/fastutil/shorts/**' exclude 'org/checkerframework/checker/**' + + relocate 'org.bstats', 'com.velocitypowered.proxy.bstats' } artifacts { archives shadowJar } - -test { - useJUnitPlatform() -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java index 182316d7d..5728ca673 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java @@ -1,82 +1,67 @@ package com.velocitypowered.proxy; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import com.velocitypowered.proxy.config.VelocityConfiguration; -import io.netty.handler.codec.http.HttpHeaderNames; -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; + +import java.io.File; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.nio.file.Paths; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPOutputStream; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.asynchttpclient.ListenableFuture; -import org.asynchttpclient.Response; +import org.bstats.MetricsBase; +import org.bstats.charts.CustomChart; +import org.bstats.charts.DrilldownPie; +import org.bstats.charts.SimplePie; +import org.bstats.charts.SingleLineChart; +import org.bstats.config.MetricsConfig; +import org.bstats.json.JsonObjectBuilder; -/** - * bStats collects some data for plugin authors. - *

- * Check out https://bStats.org/ to learn more about bStats! - */ public class Metrics { - // The version of this bStats class - private static final int B_STATS_METRICS_REVISION = 2; + private MetricsBase metricsBase; - // The url to which the data is sent - private static final String URL = "https://bstats.org/submitData/server-implementation"; + private Metrics(Logger logger, int serviceId, boolean defaultEnabled) { + File configFile = Paths.get("plugins").resolve("bStats").resolve("config.txt").toFile(); + MetricsConfig config; + try { + config = new MetricsConfig(configFile, defaultEnabled); + } catch (IOException e) { + logger.error("Failed to create bStats config", e); + return; + } - // The logger for the failed requests - private static final Logger logger = LogManager.getLogger(Metrics.class); + metricsBase = new MetricsBase( + "server-implementation", + config.getServerUUID(), + serviceId, + config.isEnabled(), + this::appendPlatformData, + jsonObjectBuilder -> { /* NOP */ }, + null, + () -> true, + logger::warn, + logger::info, + config.isLogErrorsEnabled(), + config.isLogSentDataEnabled(), + config.isLogResponseStatusTextEnabled() + ); - // Should failed requests be logged? - private boolean logFailedRequests = false; - - // The name of the server software - private final String name; - - // The plugin ID for the server software as assigned by bStats. - private final int pluginId; - - // The uuid of the server - private final String serverUuid; - - // A list with all custom charts - private final List charts = new ArrayList<>(); - - private final VelocityServer server; - - /** - * Class constructor. - * @param name The name of the server software. - * @param pluginId The plugin ID for the server software as assigned by bStats. - * @param serverUuid The uuid of the server. - * @param logFailedRequests Whether failed requests should be logged or not. - * @param server The Velocity server instance. - */ - private Metrics(String name, int pluginId, String serverUuid, boolean logFailedRequests, - VelocityServer server) { - this.name = name; - this.pluginId = pluginId; - this.serverUuid = serverUuid; - this.logFailedRequests = logFailedRequests; - this.server = server; - - // Start submitting the data - startSubmitting(); + if (!config.didExistBefore()) { + // Send an info message when the bStats config file gets created for the first time + logger.info("Velocity and some of its plugins collect metrics" + + " and send them to bStats (https://bStats.org)."); + logger.info("bStats collects some basic information for plugin" + + " authors, like how many people use"); + logger.info("their plugin and their total player count." + + " It's recommended to keep bStats enabled, but"); + logger.info("if you're not comfortable with this, you can opt-out" + + " by editing the config.txt file in"); + logger.info("the '/plugins/bStats/' folder and setting enabled to false."); + } } /** @@ -85,544 +70,69 @@ public class Metrics { * @param chart The chart to add. */ public void addCustomChart(CustomChart chart) { - if (chart == null) { - throw new IllegalArgumentException("Chart cannot be null!"); - } - charts.add(chart); + metricsBase.addCustomChart(chart); } - /** - * Starts the Scheduler which submits our data every 30 minutes. - */ - private void startSubmitting() { - final Timer timer = new Timer(true); - timer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - submitData(); - } - }, 1000, 1000 * 60 * 30); - // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough - // time to start. - // - // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! - // WARNING: Just don't do it! - } - - /** - * Gets the plugin specific data. - * - * @return The plugin specific data. - */ - private JsonObject getPluginData() { - JsonObject data = new JsonObject(); - - data.addProperty("pluginName", name); // Append the name of the server software - data.addProperty("id", pluginId); - data.addProperty("metricsRevision", B_STATS_METRICS_REVISION); - JsonArray customCharts = new JsonArray(); - for (CustomChart customChart : charts) { - // Add the data of the custom charts - JsonObject chart = customChart.getRequestJsonObject(); - if (chart == null) { // If the chart is null, we skip it - continue; - } - customCharts.add(chart); - } - data.add("customCharts", customCharts); - - return data; - } - - /** - * Gets the server specific data. - * - * @return The server specific data. - */ - private JsonObject getServerData() { - // OS specific data - String osName = System.getProperty("os.name"); - String osArch = System.getProperty("os.arch"); - String osVersion = System.getProperty("os.version"); - int coreCount = Runtime.getRuntime().availableProcessors(); - - JsonObject data = new JsonObject(); - - data.addProperty("serverUUID", serverUuid); - - data.addProperty("osName", osName); - data.addProperty("osArch", osArch); - data.addProperty("osVersion", osVersion); - data.addProperty("coreCount", coreCount); - - return data; - } - - /** - * Collects the data and sends it afterwards. - */ - private void submitData() { - final JsonObject data = getServerData(); - - JsonArray pluginData = new JsonArray(); - pluginData.add(getPluginData()); - data.add("plugins", pluginData); - - try { - // We are still in the Thread of the timer, so nothing get blocked :) - sendData(data); - } catch (Exception e) { - // Something went wrong! :( - if (logFailedRequests) { - logger.warn("Could not submit stats of {}", name, e); - } - } - } - - /** - * Sends the data to the bStats server. - * - * @param data The data to send. - * @throws Exception If the request failed. - */ - private void sendData(JsonObject data) throws Exception { - if (data == null) { - throw new IllegalArgumentException("Data cannot be null!"); - } - - // Compress the data to save bandwidth - ListenableFuture future = server.getAsyncHttpClient() - .preparePost(URL) - .addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip") - .addHeader(HttpHeaderNames.ACCEPT, "application/json") - .addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json") - .setBody(createResponseBody(data)) - .execute(); - future.addListener(() -> { - if (logFailedRequests) { - try { - Response r = future.get(); - if (r.getStatusCode() != 429) { - logger.error("Got HTTP status code {} when sending metrics to bStats", - r.getStatusCode()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - logger.error("Unable to send metrics to bStats", e); - } - } - }, null); - } - - private static byte[] createResponseBody(JsonObject object) throws IOException { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try (Writer writer = - new BufferedWriter( - new OutputStreamWriter( - new GZIPOutputStream(os), StandardCharsets.UTF_8 - ) - ) - ) { - VelocityServer.GENERAL_GSON.toJson(object, writer); - } - return os.toByteArray(); - } - - /** - * Represents a custom chart. - */ - public abstract static class CustomChart { - - // The id of the chart - final String chartId; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - */ - CustomChart(String chartId) { - if (chartId == null || chartId.isEmpty()) { - throw new IllegalArgumentException("ChartId cannot be null or empty!"); - } - this.chartId = chartId; - } - - private JsonObject getRequestJsonObject() { - JsonObject chart = new JsonObject(); - chart.addProperty("chartId", chartId); - try { - JsonObject data = getChartData(); - if (data == null) { - // If the data is null we don't send the chart. - return null; - } - chart.add("data", data); - } catch (Throwable t) { - return null; - } - return chart; - } - - protected abstract JsonObject getChartData() throws Exception; - - } - - /** - * Represents a custom simple pie. - */ - public static class SimplePie extends CustomChart { - - private final Callable callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SimplePie(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject data = new JsonObject(); - String value = callable.call(); - if (value == null || value.isEmpty()) { - // Null = skip the chart - return null; - } - data.addProperty("value", value); - return data; - } - } - - /** - * Represents a custom advanced pie. - */ - public static class AdvancedPie extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public AdvancedPie(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - continue; // Skip this invalid - } - allSkipped = false; - values.addProperty(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - data.add("values", values); - return data; - } - } - - /** - * Represents a custom drilldown pie. - */ - public static class DrilldownPie extends CustomChart { - - private final Callable>> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public DrilldownPie(String chartId, Callable>> callable) { - super(chartId); - this.callable = callable; - } - - @Override - public JsonObject getChartData() throws Exception { - Map> map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean reallyAllSkipped = true; - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - for (Map.Entry> entryValues : map.entrySet()) { - JsonObject value = new JsonObject(); - boolean allSkipped = true; - for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { - value.addProperty(valueEntry.getKey(), valueEntry.getValue()); - allSkipped = false; - } - if (!allSkipped) { - reallyAllSkipped = false; - values.add(entryValues.getKey(), value); - } - } - if (reallyAllSkipped) { - // Null = skip the chart - return null; - } - data.add("values", values); - return data; - } - } - - /** - * Represents a custom single line chart. - */ - public static class SingleLineChart extends CustomChart { - - private final Callable callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SingleLineChart(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject data = new JsonObject(); - int value = callable.call(); - if (value == 0) { - // Null = skip the chart - return null; - } - data.addProperty("value", value); - return data; - } - - } - - /** - * Represents a custom multi line chart. - */ - public static class MultiLineChart extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public MultiLineChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - continue; // Skip this invalid - } - allSkipped = false; - values.addProperty(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - data.add("values", values); - return data; - } - - } - - /** - * Represents a custom simple bar chart. - */ - public static class SimpleBarChart extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SimpleBarChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - for (Map.Entry entry : map.entrySet()) { - JsonArray categoryValues = new JsonArray(); - categoryValues.add(entry.getValue()); - values.add(entry.getKey(), categoryValues); - } - data.add("values", values); - return data; - } - - } - - /** - * Represents a custom advanced bar chart. - */ - public static class AdvancedBarChart extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public AdvancedBarChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject values = new JsonObject(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue().length == 0) { - continue; // Skip this invalid - } - allSkipped = false; - JsonArray categoryValues = new JsonArray(); - for (int categoryValue : entry.getValue()) { - categoryValues.add(categoryValue); - } - values.add(entry.getKey(), categoryValues); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - JsonObject data = new JsonObject(); - data.add("values", values); - return data; - } - + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); } static class VelocityMetrics { + + private static final Logger logger = LogManager.getLogger(Metrics.class); + static void startMetrics(VelocityServer server, VelocityConfiguration.Metrics metricsConfig) { - if (!metricsConfig.isFromConfig()) { - // Log an informational message. - logger.info("Velocity collects metrics and sends them to bStats (https://bstats.org)."); - logger.info("bStats collects some basic information like how many people use Velocity and"); - logger.info("their player count. This has no impact on performance and this data does not"); - logger.info("identify your server in any way. However, you may opt-out by editing your"); - logger.info("velocity.toml and setting enabled = false in the [metrics] section."); - } + Metrics metrics = new Metrics(logger, 4752, metricsConfig.isEnabled()); - // Load the data - String serverUuid = metricsConfig.getId(); - boolean logFailedRequests = metricsConfig.isLogFailure(); - // Only start Metrics, if it's enabled in the config - if (metricsConfig.isEnabled()) { - Metrics metrics = new Metrics("Velocity", 4752, serverUuid, logFailedRequests, server); + metrics.addCustomChart( + new SingleLineChart("players", server::getPlayerCount) + ); + metrics.addCustomChart( + new SingleLineChart("managed_servers", () -> server.getAllServers().size()) + ); + metrics.addCustomChart( + new SimplePie("online_mode", + () -> server.getConfiguration().isOnlineMode() ? "online" : "offline") + ); + metrics.addCustomChart(new SimplePie("velocity_version", + () -> server.getVersion().getVersion())); - metrics.addCustomChart( - new Metrics.SingleLineChart("players", server::getPlayerCount) - ); - metrics.addCustomChart( - new Metrics.SingleLineChart("managed_servers", () -> server.getAllServers().size()) - ); - metrics.addCustomChart( - new Metrics.SimplePie("online_mode", - () -> server.getConfiguration().isOnlineMode() ? "online" : "offline") - ); - metrics.addCustomChart(new Metrics.SimplePie("velocity_version", - () -> server.getVersion().getVersion())); + metrics.addCustomChart(new DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); + String javaVersion = System.getProperty("java.version"); + Map entry = new HashMap<>(); + entry.put(javaVersion, 1); - metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { - Map> map = new HashMap<>(); - String javaVersion = System.getProperty("java.version"); - Map entry = new HashMap<>(); - entry.put(javaVersion, 1); + // http://openjdk.java.net/jeps/223 + // Java decided to change their versioning scheme and in doing so modified the + // java.version system property to return $major[.$minor][.$security][-ea], as opposed to + // 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1", + // otherwise, 9+ + String majorVersion = javaVersion.split("\\.")[0]; + String release; - // http://openjdk.java.net/jeps/223 - // Java decided to change their versioning scheme and in doing so modified the - // java.version system property to return $major[.$minor][.$security][-ea], as opposed to - // 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1", - // otherwise, 9+ - String majorVersion = javaVersion.split("\\.")[0]; - String release; + int indexOf = javaVersion.lastIndexOf('.'); - int indexOf = javaVersion.lastIndexOf('.'); - - if (majorVersion.equals("1")) { - release = "Java " + javaVersion.substring(0, indexOf); - } else { - // of course, it really wouldn't be all that simple if they didn't add a quirk, now - // would it valid strings for the major may potentially include values such as -ea to - // denote a pre release - Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); - if (versionMatcher.find()) { - majorVersion = versionMatcher.group(0); - } - release = "Java " + majorVersion; + if (majorVersion.equals("1")) { + release = "Java " + javaVersion.substring(0, indexOf); + } else { + // of course, it really wouldn't be all that simple if they didn't add a quirk, now + // would it valid strings for the major may potentially include values such as -ea to + // denote a pre release + Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); + if (versionMatcher.find()) { + majorVersion = versionMatcher.group(0); } - map.put(release, entry); - - return map; - })); - } + release = "Java " + majorVersion; + } + map.put(release, entry); + return map; + })); } } -} + +} \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java index 85811abfd..a9599bd4f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java @@ -8,9 +8,12 @@ import org.apache.logging.log4j.Logger; public class Velocity { - private static final Logger logger = LogManager.getLogger(Velocity.class); + private static final Logger logger; static { + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + logger = LogManager.getLogger(Velocity.class); + // By default, Netty allocates 16MiB arenas for the PooledByteBufAllocator. This is too much // memory for Minecraft, which imposes a maximum packet size of 2MiB! We'll use 4MiB as a more // sane default. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java index c6f4d0dcc..c0786bbb2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java @@ -59,6 +59,9 @@ public interface CommandNodeFactory { (context, builder) -> { I invocation = createInvocation(context); + if (!command.hasPermission(invocation)) { + return builder.buildFuture(); + } return command.suggestAsync(invocation).thenApply(values -> { for (String value : values) { builder.suggest(value); 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 3218960b9..3297f457b 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 @@ -38,6 +38,7 @@ import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -188,6 +189,7 @@ public class VelocityCommand implements SimpleCommand { private static class Info implements SubCommand { + private static final TextColor VELOCITY_COLOR = TextColor.fromHexString("#09add3"); private final ProxyServer server; private Info(ProxyServer server) { @@ -205,11 +207,11 @@ public class VelocityCommand implements SimpleCommand { TextComponent velocity = Component.text().content(version.getName() + " ") .decoration(TextDecoration.BOLD, true) - .color(NamedTextColor.DARK_AQUA) + .color(VELOCITY_COLOR) .append(Component.text(version.getVersion()).decoration(TextDecoration.BOLD, false)) .build(); TextComponent copyright = Component - .text("Copyright 2018-2020 " + version.getVendor() + ". " + version.getName() + .text("Copyright 2018-2021 " + version.getVendor() + ". " + version.getName() + " is freely licensed under the terms of the MIT License."); source.sendMessage(Identity.nil(), velocity); source.sendMessage(Identity.nil(), copyright); 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 12ef585d5..e994b3912 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; -import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -426,11 +425,6 @@ public class VelocityConfiguration implements ProxyConfig { } forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8); - if (!config.contains("metrics.id") || config.get("metrics.id").isEmpty()) { - config.set("metrics.id", UUID.randomUUID().toString()); - mustResave = true; - } - if (mustResave) { config.save(); } @@ -448,7 +442,7 @@ public class VelocityConfiguration implements ProxyConfig { PingPassthroughMode.DISABLED); String bind = config.getOrElse("bind", "0.0.0.0:25577"); - String motd = config.getOrElse("motd", "&3A Velocity Server"); + String motd = config.getOrElse("motd", " add3A Velocity Server"); int maxPlayers = config.getIntOrElse("show-max-players", 500); Boolean onlineMode = config.getOrElse("online-mode", true); Boolean announceForge = config.getOrElse("announce-forge", true); @@ -636,7 +630,11 @@ public class VelocityConfiguration implements ProxyConfig { this.loginRatelimit = config.getIntOrElse("login-ratelimit", 3000); this.connectionTimeout = config.getIntOrElse("connection-timeout", 5000); this.readTimeout = config.getIntOrElse("read-timeout", 30000); - this.proxyProtocol = config.getOrElse("proxy-protocol", false); + if (config.contains("haproxy-protocol")) { + this.proxyProtocol = config.getOrElse("haproxy-protocol", false); + } else { + this.proxyProtocol = config.getOrElse("proxy-protocol", false); + } this.tcpFastOpen = config.getOrElse("tcp-fast-open", false); this.bungeePluginMessageChannel = config.getOrElse("bungee-plugin-message-channel", true); this.showPingRequests = config.getOrElse("show-ping-requests", false); @@ -769,39 +767,16 @@ public class VelocityConfiguration implements ProxyConfig { public static class Metrics { private boolean enabled = true; - private String id = UUID.randomUUID().toString(); - private boolean logFailure = false; - - private boolean fromConfig; - - private Metrics() { - this.fromConfig = false; - } private Metrics(CommentedConfig toml) { if (toml != null) { - this.enabled = toml.getOrElse("enabled", false); - this.id = toml.getOrElse("id", UUID.randomUUID().toString()); - this.logFailure = toml.getOrElse("log-failure", false); - this.fromConfig = true; + this.enabled = toml.getOrElse("enabled", true); } } public boolean isEnabled() { return enabled; } - - public String getId() { - return id; - } - - public boolean isLogFailure() { - return logFailure; - } - - public boolean isFromConfig() { - return fromConfig; - } } public static class Messages { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index 0935631a5..100d8116a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -196,6 +196,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void write(Object msg) { if (channel.isActive()) { channel.writeAndFlush(msg, channel.voidPromise()); + } else { + ReferenceCountUtil.release(msg); } } @@ -206,6 +208,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void delayedWrite(Object msg) { if (channel.isActive()) { channel.write(msg, channel.voidPromise()); + } else { + ReferenceCountUtil.release(msg); } } @@ -379,16 +383,25 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (threshold == -1) { channel.pipeline().remove(COMPRESSION_DECODER); channel.pipeline().remove(COMPRESSION_ENCODER); - return; + } else { + MinecraftCompressDecoder decoder = (MinecraftCompressDecoder) channel.pipeline() + .get(COMPRESSION_DECODER); + MinecraftCompressEncoder encoder = (MinecraftCompressEncoder) channel.pipeline() + .get(COMPRESSION_ENCODER); + if (decoder != null && encoder != null) { + decoder.setThreshold(threshold); + encoder.setThreshold(threshold); + } else { + int level = server.getConfiguration().getCompressionLevel(); + VelocityCompressor compressor = Natives.compress.get().create(level); + + encoder = new MinecraftCompressEncoder(threshold, compressor); + decoder = new MinecraftCompressDecoder(threshold, compressor); + + channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); + channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); + } } - - int level = server.getConfiguration().getCompressionLevel(); - VelocityCompressor compressor = Natives.compress.get().create(level); - MinecraftCompressEncoder encoder = new MinecraftCompressEncoder(threshold, compressor); - MinecraftCompressDecoder decoder = new MinecraftCompressDecoder(threshold, compressor); - - channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); - channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); } /** 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 ea460c17f..cd6c200f2 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 @@ -30,6 +30,7 @@ import com.velocitypowered.proxy.network.packet.serverbound.ServerboundPluginMes import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; import io.netty.handler.timeout.ReadTimeoutException; import java.util.Collection; import org.apache.logging.log4j.LogManager; @@ -38,6 +39,8 @@ import org.apache.logging.log4j.Logger; public class BackendPlaySessionHandler implements MinecraftSessionHandler { private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class); + private static final boolean BACKPRESSURE_LOG = Boolean + .getBoolean("velocity.log-server-backpressure"); private final VelocityServer server; private final VelocityServerConnection serverConn; private final ClientPlaySessionHandler playerSessionHandler; @@ -64,10 +67,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { serverConn.getServer().addPlayer(serverConn.getPlayer()); - MinecraftConnection serverMc = serverConn.ensureConnected(); - serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), - ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())), ServerboundPluginMessagePacket.FACTORY - )); + + if (server.getConfiguration().isBungeePluginChannelEnabled()) { + MinecraftConnection serverMc = serverConn.ensureConnected(); + serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), + ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())) + )); + } } @Override @@ -286,4 +292,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } } } + + @Override + public void writabilityChanged() { + Channel serverChan = serverConn.ensureConnected().getChannel(); + boolean writable = serverChan.isWritable(); + + if (BACKPRESSURE_LOG) { + if (writable) { + logger.info("{} is not writable, not auto-reading player connection data", this.serverConn); + } else { + logger.info("{} is writable, will auto-read player connection data", this.serverConn); + } + } + + playerConnection.setAutoReading(writable); + } } 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 1edbbf87d..0e19d4fd9 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 @@ -262,75 +262,52 @@ public class BungeeCordMessageResponder { }); } - private ByteBuf prepareForwardMessage(ByteBufDataInput in) { - String channel = in.readUTF(); - short messageLength = in.readShort(); - - ByteBuf buf = Unpooled.buffer(); - ByteBufDataOutput forwarded = new ByteBufDataOutput(buf); - forwarded.writeUTF(channel); - forwarded.writeShort(messageLength); - buf.writeBytes(in.unwrap().readSlice(messageLength)); - return buf; - } - private void processForwardToPlayer(ByteBufDataInput in) { - proxy.getPlayer(in.readUTF()) - .flatMap(Player::getCurrentServer) - .ifPresent(server -> sendServerResponse(player, prepareForwardMessage(in))); + Optional player = proxy.getPlayer(in.readUTF()); + if (player.isPresent()) { + ByteBuf toForward = in.unwrap().copy(); + sendServerResponse((ConnectedPlayer) player.get(), toForward); + } } private void processForwardToServer(ByteBufDataInput in) { String target = in.readUTF(); - ByteBuf toForward = prepareForwardMessage(in); + ByteBuf toForward = in.unwrap().copy(); if (target.equals("ALL")) { - ByteBuf unreleasableForward = Unpooled.unreleasableBuffer(toForward); try { for (RegisteredServer rs : proxy.getAllServers()) { - ((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL, unreleasableForward); + ((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL, + toForward.retainedSlice()); } } finally { toForward.release(); } } else { - proxy.getServer(target).ifPresent(rs -> ((VelocityRegisteredServer) rs) - .sendPluginMessage(LEGACY_CHANNEL, toForward)); + Optional server = proxy.getServer(target); + if (server.isPresent()) { + ((VelocityRegisteredServer) server.get()).sendPluginMessage(LEGACY_CHANNEL, toForward); + } else { + toForward.release(); + } } } - // Note: this method will always release the buffer! - private void sendResponseOnConnection(ByteBuf buf) { - sendServerResponse(this.player, buf); - } - static String getBungeeCordChannel(ProtocolVersion version) { return version.gte(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId() : LEGACY_CHANNEL.getId(); } + // Note: this method will always release the buffer! + private void sendResponseOnConnection(ByteBuf buf) { + sendServerResponse(this.player, buf); + } + // Note: this method will always release the buffer! private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) { MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected(); String chan = getBungeeCordChannel(serverConnection.getProtocolVersion()); - - ServerboundPluginMessagePacket msg = null; - boolean released = false; - - try { - VelocityServerConnection vsc = player.getConnectedServer(); - if (vsc == null) { - return; - } - - MinecraftConnection serverConn = vsc.ensureConnected(); - msg = new ServerboundPluginMessagePacket(chan, buf); - serverConn.write(msg); - released = true; - } finally { - if (!released && msg != null) { - msg.release(); - } - } + ServerboundPluginMessagePacket msg = new ServerboundPluginMessagePacket(chan, buf); + serverConnection.write(msg); } boolean process(AbstractPluginMessagePacket message) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index ab15e0f09..534b356a8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -32,7 +32,9 @@ import io.netty.channel.ChannelFutureListener; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.UnaryOperator; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -47,8 +49,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private boolean hasCompletedJoin = false; private boolean gracefulDisconnect = false; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; - private long lastPingId; - private long lastPingSent; + private final Map pendingPings = new HashMap<>(); private @MonotonicNonNull DimensionRegistry activeDimensionRegistry; /** @@ -112,7 +113,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, return getHandshakeRemoteAddress(); } StringBuilder data = new StringBuilder() - .append(getHandshakeRemoteAddress()) + .append(registeredServer.getServerInfo().getAddress().getHostString()) .append('\0') .append(((InetSocketAddress) proxyPlayer.getRemoteAddress()).getHostString()) .append('\0') @@ -259,21 +260,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, return gracefulDisconnect; } - public long getLastPingId() { - return lastPingId; - } - - public long getLastPingSent() { - return lastPingSent; - } - - void setLastPingId(long lastPingId) { - this.lastPingId = lastPingId; - this.lastPingSent = System.currentTimeMillis(); - } - - public void resetLastPingId() { - this.lastPingId = -1; + public Map getPendingPings() { + return pendingPings; } /** 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 ba431ee41..4152f6bb9 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 @@ -5,13 +5,17 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.network.PluginMessageUtil.constructChannelsPacket; +import com.google.common.collect.ImmutableList; import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult; import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -102,12 +106,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerboundKeepAlivePacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); - if (serverConnection != null && packet.getRandomId() == serverConnection.getLastPingId()) { - MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null) { - player.setPing(System.currentTimeMillis() - serverConnection.getLastPingSent()); - smc.write(packet); - serverConnection.resetLastPingId(); + if (serverConnection != null) { + Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + player.setPing(System.currentTimeMillis() - sentTime); + smc.write(packet); + } } } return true; @@ -191,7 +197,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { logger.warn("A plugin message was received while the backend server was not " + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { - player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); + List channels = PluginMessageUtil.getChannels(packet); + player.getKnownChannels().addAll(channels); + List channelIdentifiers = new ArrayList<>(); + for (String channel : channels) { + try { + channelIdentifiers.add(MinecraftChannelIdentifier.from(channel)); + } catch (IllegalArgumentException e) { + channelIdentifiers.add(new LegacyChannelIdentifier(channel)); + } + } + server.getEventManager().fireAndForget(new PlayerChannelRegisterEvent(player, + ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); @@ -303,7 +320,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { boolean writable = player.getConnection().getChannel().isWritable(); if (!writable) { - // We might have packets queued for the server, so flush them now to free up memory. + // We might have packets queued from the server, so flush them now to free up memory. player.getConnection().flush(); } 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 412e38646..e199e01a3 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 @@ -59,6 +59,7 @@ import java.net.SocketAddress; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -103,6 +104,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private @Nullable VelocityServerConnection connectionInFlight; private @Nullable PlayerSettings settings; private @Nullable ModInfo modInfo; + private Component playerListHeader = Component.empty(); + private Component playerListFooter = Component.empty(); private final VelocityTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; @@ -113,11 +116,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, @Nullable InetSocketAddress virtualHost, boolean onlineMode) { this.server = server; - if (connection.getProtocolVersion().gte(ProtocolVersion.MINECRAFT_1_8)) { - this.tabList = new VelocityTabList(connection); - } else { - this.tabList = new VelocityTabListLegacy(connection); - } this.profile = profile; this.connection = connection; this.virtualHost = virtualHost; @@ -125,6 +123,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { this.connectionPhase = connection.getType().getInitialClientPhase(); this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS); this.onlineMode = onlineMode; + + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.tabList = new VelocityTabList(this); + } else { + this.tabList = new VelocityTabListLegacy(this); + } } @Override @@ -265,46 +269,85 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } @Override - public void showTitle(net.kyori.adventure.title.@NonNull Title title) { - GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(this - .getProtocolVersion()); + public Component getPlayerListHeader() { + return this.playerListHeader; + } - connection.delayedWrite(new ClientboundTitlePacket( - ClientboundTitlePacket.SET_TITLE, - serializer.serialize(title.title()) - )); + @Override + public Component getPlayerListFooter() { + return this.playerListFooter; + } - connection.delayedWrite(new ClientboundTitlePacket( - ClientboundTitlePacket.SET_SUBTITLE, - serializer.serialize(title.subtitle()) - )); + @Override + public void sendPlayerListHeader(@NonNull final Component header) { + this.sendPlayerListHeaderAndFooter(header, this.playerListFooter); + } - net.kyori.adventure.title.Title.Times times = title.times(); - if (times != null) { - connection.delayedWrite(ClientboundTitlePacket.times(this.getProtocolVersion(), times)); + @Override + public void sendPlayerListFooter(@NonNull final Component footer) { + this.sendPlayerListHeaderAndFooter(this.playerListHeader, footer); + } + + @Override + public void sendPlayerListHeaderAndFooter(final Component header, final Component footer) { + this.playerListHeader = Objects.requireNonNull(header, "header"); + this.playerListFooter = Objects.requireNonNull(footer, "footer"); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.connection.write(HeaderAndFooter.create(header, footer, this.getProtocolVersion())); } + } - connection.flush(); + @Override + public void showTitle(net.kyori.adventure.title.@NonNull Title title) { + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(this + .getProtocolVersion()); + + connection.delayedWrite(new ClientboundTitlePacket( + ClientboundTitlePacket.SET_TITLE, + serializer.serialize(title.title()) + )); + + connection.delayedWrite(new ClientboundTitlePacket( + ClientboundTitlePacket.SET_SUBTITLE, + serializer.serialize(title.subtitle()) + )); + + net.kyori.adventure.title.Title.Times times = title.times(); + if (times != null) { + connection.delayedWrite(ClientboundTitlePacket.times(this.getProtocolVersion(), times)); + } + + connection.flush(); + } } @Override public void clearTitle() { - connection.write(ClientboundTitlePacket.hide(this.getProtocolVersion())); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + connection.write(ClientboundTitlePacket.hide(this.getProtocolVersion())); + } } @Override public void resetTitle() { - connection.write(ClientboundTitlePacket.reset(this.getProtocolVersion())); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + connection.write(ClientboundTitlePacket.reset(this.getProtocolVersion())); + } } @Override public void hideBossBar(@NonNull BossBar bar) { - this.server.getBossBarManager().removeBossBar(this, bar); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + this.server.getBossBarManager().removeBossBar(this, bar); + } } @Override public void showBossBar(@NonNull BossBar bar) { - this.server.getBossBarManager().addBossBar(this, bar); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + this.server.getBossBarManager().addBossBar(this, bar); + } } @Override @@ -317,6 +360,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { this.profile = profile.withProperties(properties); } + @Override + public void clearHeaderAndFooter() { + tabList.clearHeaderAndFooter(); + } + @Override public VelocityTabList getTabList() { return tabList; @@ -412,7 +460,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { logger.error("{}: kicked from server {}: {}", this, server.getServerInfo().getName(), plainTextReason); handleConnectionException(server, disconnectReason, Component.text() - .append(messages.getKickPrefix(server.getServerInfo().getName())) + .append(messages.getKickPrefix(server.getServerInfo().getName()) + .colorIfAbsent(NamedTextColor.RED)) .color(NamedTextColor.RED) .append(disconnectReason) .build(), safe); @@ -420,8 +469,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { logger.error("{}: disconnected while connecting to {}: {}", this, server.getServerInfo().getName(), plainTextReason); handleConnectionException(server, disconnectReason, Component.text() - .append(messages.getDisconnectPrefix(server.getServerInfo().getName())) - .color(NamedTextColor.RED) + .append(messages.getDisconnectPrefix(server.getServerInfo().getName()) + .colorIfAbsent(NamedTextColor.RED)) .append(disconnectReason) .build(), safe); } @@ -678,7 +727,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { public void sendResourcePack(String url) { Preconditions.checkNotNull(url, "url"); - connection.write(new ClientboundResourcePackRequestPacket(url, "")); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + connection.write(new ClientboundResourcePackRequestPacket(url, "")); + } } @Override @@ -687,7 +738,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { Preconditions.checkNotNull(hash, "hash"); Preconditions.checkArgument(hash.length == 20, "Hash length is not 20"); - connection.write(new ClientboundResourcePackRequestPacket(url, ByteBufUtil.hexDump(hash))); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + connection.write(new ClientboundResourcePackRequestPacket(url, ByteBufUtil.hexDump(hash))); + } } /** 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 3d91382ba..d49f04dc9 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 @@ -36,6 +36,7 @@ import io.netty.buffer.ByteBuf; import java.net.InetSocketAddress; import java.security.GeneralSecurityException; import java.security.KeyPair; +import java.security.MessageDigest; import java.util.Arrays; import java.util.Optional; import java.util.UUID; @@ -90,7 +91,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { try { KeyPair serverKeyPair = server.getServerKeyPair(); byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken()); - if (!Arrays.equals(verify, decryptedVerifyToken)) { + if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) { throw new IllegalStateException("Unable to successfully decrypt the verification token."); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java index e138c2b59..1f6d09d3c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java @@ -16,6 +16,8 @@ import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; +import java.io.DataInput; +import java.io.DataOutput; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -227,11 +229,12 @@ public enum ProtocolUtils { /** * Reads a {@link net.kyori.adventure.nbt.CompoundBinaryTag} from the {@code buf}. * @param buf the buffer to read from + * @param reader the NBT reader to use * @return {@link net.kyori.adventure.nbt.CompoundBinaryTag} the CompoundTag from the buffer */ - public static CompoundBinaryTag readCompoundTag(ByteBuf buf) { + public static CompoundBinaryTag readCompoundTag(ByteBuf buf, BinaryTagIO.Reader reader) { try { - return BinaryTagIO.readDataInput(new ByteBufInputStream(buf)); + return reader.read((DataInput) new ByteBufInputStream(buf)); } catch (IOException thrown) { throw new DecoderException( "Unable to parse NBT CompoundTag, full error: " + thrown.getMessage()); @@ -245,7 +248,7 @@ public enum ProtocolUtils { */ public static void writeCompoundTag(ByteBuf buf, CompoundBinaryTag compoundTag) { try { - BinaryTagIO.writeDataOutput(compoundTag, new ByteBufOutputStream(buf)); + BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(buf)); } catch (IOException e) { throw new EncoderException("Unable to encode NBT CompoundTag"); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundAvailableCommandsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundAvailableCommandsPacket.java index b8094acaf..165cb8c07 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundAvailableCommandsPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundAvailableCommandsPacket.java @@ -25,6 +25,7 @@ import com.velocitypowered.proxy.network.packet.PacketHandler; import com.velocitypowered.proxy.network.packet.PacketReader; import com.velocitypowered.proxy.network.serialization.brigadier.ArgumentPropertyRegistry; import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap; import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import java.util.ArrayDeque; @@ -99,7 +100,8 @@ public class ClientboundAvailableCommandsPacket implements Packet { public void encode(ByteBuf buf, PacketDirection direction, ProtocolVersion protocolVersion) { // Assign all the children an index. Deque> childrenQueue = new ArrayDeque<>(ImmutableList.of(rootNode)); - Object2IntMap> idMappings = new Object2IntLinkedOpenHashMap<>(); + Object2IntMap> idMappings = new Object2IntLinkedOpenCustomHashMap<>( + IdentityHashStrategy.instance()); while (!childrenQueue.isEmpty()) { CommandNode child = childrenQueue.poll(); if (!idMappings.containsKey(child)) { @@ -207,6 +209,7 @@ public class ClientboundAvailableCommandsPacket implements Packet { private final int redirectTo; private final @Nullable ArgumentBuilder args; private @MonotonicNonNull CommandNode built; + private boolean validated; private WireNode(int idx, byte flags, int[] children, int redirectTo, @Nullable ArgumentBuilder args) { @@ -215,18 +218,34 @@ public class ClientboundAvailableCommandsPacket implements Packet { this.children = children; this.redirectTo = redirectTo; this.args = args; + this.validated = false; + } + + void validate(WireNode[] wireNodes) { + // Ensure all children exist. Note that we delay checking if the node has been built yet; + // that needs to come after this node is built. + for (int child : children) { + if (child < 0 || child >= wireNodes.length) { + throw new IllegalStateException("Node points to non-existent index " + child); + } + } + + if (redirectTo != -1) { + if (redirectTo < 0 || redirectTo >= wireNodes.length) { + throw new IllegalStateException("Redirect node points to non-existent index " + + redirectTo); + } + } + + this.validated = true; } boolean toNode(WireNode[] wireNodes) { - if (this.built == null) { - // Ensure all children exist. Note that we delay checking if the node has been built yet; - // that needs to come after this node is built. - for (int child : children) { - if (child >= wireNodes.length) { - throw new IllegalStateException("Node points to non-existent index " + redirectTo); - } - } + if (!this.validated) { + this.validate(wireNodes); + } + if (this.built == null) { int type = flags & FLAG_NODE_TYPE; if (type == NODE_TYPE_ROOT) { this.built = new RootCommandNode<>(); @@ -237,10 +256,6 @@ public class ClientboundAvailableCommandsPacket implements Packet { // Add any redirects if (redirectTo != -1) { - if (redirectTo >= wireNodes.length) { - throw new IllegalStateException("Node points to non-existent index " + redirectTo); - } - WireNode redirect = wireNodes[redirectTo]; if (redirect.built != null) { args.redirect(redirect.built); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundJoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundJoinGamePacket.java index 81fdb4650..a3801223c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundJoinGamePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundJoinGamePacket.java @@ -12,6 +12,7 @@ import com.velocitypowered.proxy.network.packet.PacketDirection; import com.velocitypowered.proxy.network.packet.PacketHandler; import com.velocitypowered.proxy.network.packet.PacketReader; import io.netty.buffer.ByteBuf; +import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.BinaryTagTypes; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.ListBinaryTag; @@ -20,6 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class ClientboundJoinGamePacket implements Packet { public static final PacketReader DECODER = PacketReader.method(ClientboundJoinGamePacket::new); + private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(2 * 1024 * 1024); private int entityId; private short gamemode; private int dimension; @@ -37,14 +39,136 @@ public class ClientboundJoinGamePacket implements Packet { private short previousGamemode; // 1.16+ private CompoundBinaryTag biomeRegistry; // 1.16.2+ - public void withDimension(int dimension) { - this.dimension = dimension; + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public short getGamemode() { + return gamemode; + } + + public void setGamemode(short gamemode) { + this.gamemode = gamemode; + } + + public int getDimension() { + return dimension; } public void setDimension(int dimension) { this.dimension = dimension; } + public long getPartialHashedSeed() { + return partialHashedSeed; + } + + public short getDifficulty() { + return difficulty; + } + + public void setDifficulty(short difficulty) { + this.difficulty = difficulty; + } + + public int getMaxPlayers() { + return maxPlayers; + } + + public void setMaxPlayers(int maxPlayers) { + this.maxPlayers = maxPlayers; + } + + public @Nullable String getLevelType() { + return levelType; + } + + public void setLevelType(String levelType) { + this.levelType = levelType; + } + + public int getViewDistance() { + return viewDistance; + } + + public void setViewDistance(int viewDistance) { + this.viewDistance = viewDistance; + } + + public boolean isReducedDebugInfo() { + return reducedDebugInfo; + } + + public void setReducedDebugInfo(boolean reducedDebugInfo) { + this.reducedDebugInfo = reducedDebugInfo; + } + + public DimensionInfo getDimensionInfo() { + return dimensionInfo; + } + + public void setDimensionInfo(DimensionInfo dimensionInfo) { + this.dimensionInfo = dimensionInfo; + } + + public DimensionRegistry getDimensionRegistry() { + return dimensionRegistry; + } + + public void setDimensionRegistry(DimensionRegistry dimensionRegistry) { + this.dimensionRegistry = dimensionRegistry; + } + + public short getPreviousGamemode() { + return previousGamemode; + } + + public void setPreviousGamemode(short previousGamemode) { + this.previousGamemode = previousGamemode; + } + + public boolean getIsHardcore() { + return isHardcore; + } + + public void setIsHardcore(boolean isHardcore) { + this.isHardcore = isHardcore; + } + + public CompoundBinaryTag getBiomeRegistry() { + return biomeRegistry; + } + + public void setBiomeRegistry(CompoundBinaryTag biomeRegistry) { + this.biomeRegistry = biomeRegistry; + } + + public DimensionData getCurrentDimensionData() { + return currentDimensionData; + } + + @Override + public String toString() { + return "JoinGame{" + + "entityId=" + entityId + + ", gamemode=" + gamemode + + ", dimension=" + dimension + + ", partialHashedSeed=" + partialHashedSeed + + ", difficulty=" + difficulty + + ", maxPlayers=" + maxPlayers + + ", levelType='" + levelType + '\'' + + ", viewDistance=" + viewDistance + + ", reducedDebugInfo=" + reducedDebugInfo + + ", dimensionRegistry='" + dimensionRegistry + '\'' + + ", dimensionInfo='" + dimensionInfo + '\'' + + ", previousGamemode=" + previousGamemode + + '}'; + } + @Override public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) { this.entityId = buf.readInt(); @@ -61,7 +185,7 @@ public class ClientboundJoinGamePacket implements Packet { if (version.gte(ProtocolVersion.MINECRAFT_1_16)) { this.previousGamemode = buf.readByte(); ImmutableSet levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); - CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf); + CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); ListBinaryTag dimensionRegistryContainer = null; if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) { dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type") @@ -74,8 +198,8 @@ public class ClientboundJoinGamePacket implements Packet { ImmutableSet readData = DimensionRegistry.fromGameData(dimensionRegistryContainer, version); this.dimensionRegistry = new DimensionRegistry(readData, levelNames); - if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) { - CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); dimensionIdentifier = ProtocolUtils.readString(buf); this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version) .annotateWith(dimensionIdentifier, null); @@ -186,66 +310,6 @@ public class ClientboundJoinGamePacket implements Packet { } } - public int getEntityId() { - return entityId; - } - - public short getGamemode() { - return gamemode; - } - - public int getDimension() { - return dimension; - } - - public long getPartialHashedSeed() { - return partialHashedSeed; - } - - public short getDifficulty() { - return difficulty; - } - - public int getMaxPlayers() { - return maxPlayers; - } - - public @Nullable String getLevelType() { - return levelType; - } - - public int getViewDistance() { - return viewDistance; - } - - public boolean isReducedDebugInfo() { - return reducedDebugInfo; - } - - public DimensionInfo getDimensionInfo() { - return dimensionInfo; - } - - public DimensionRegistry getDimensionRegistry() { - return dimensionRegistry; - } - - public short getPreviousGamemode() { - return previousGamemode; - } - - public boolean getIsHardcore() { - return isHardcore; - } - - public CompoundBinaryTag getBiomeRegistry() { - return biomeRegistry; - } - - public DimensionData getCurrentDimensionData() { - return currentDimensionData; - } - @Override public boolean handle(PacketHandler handler) { return handler.handle(this); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundRespawnPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundRespawnPacket.java index 03b46b847..1bb449c00 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundRespawnPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundRespawnPacket.java @@ -10,6 +10,7 @@ import com.velocitypowered.proxy.network.packet.PacketDirection; import com.velocitypowered.proxy.network.packet.PacketHandler; import com.velocitypowered.proxy.network.packet.PacketReader; import io.netty.buffer.ByteBuf; +import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; public class ClientboundRespawnPacket implements Packet { @@ -102,9 +103,9 @@ public class ClientboundRespawnPacket implements Packet { public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) { String dimensionIdentifier = null; String levelName = null; - if (version.gte(ProtocolVersion.MINECRAFT_1_16)) { - if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) { - CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader()); dimensionIdentifier = ProtocolUtils.readString(buf); this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version) .annotateWith(dimensionIdentifier, null); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressDecoder.java index 7285991e8..1bc29da54 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressDecoder.java @@ -20,7 +20,7 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder { Boolean.getBoolean("velocity.increased-compression-cap") ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; - private final int threshold; + private int threshold; private final VelocityCompressor compressor; public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) { @@ -60,4 +60,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder { public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { compressor.close(); } + + public void setThreshold(int threshold) { + this.threshold = threshold; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java index 0912c437e..430bae6f7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java @@ -9,7 +9,7 @@ import io.netty.handler.codec.MessageToByteEncoder; public class MinecraftCompressEncoder extends MessageToByteEncoder { - private final int threshold; + private int threshold; private final VelocityCompressor compressor; public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) { @@ -20,7 +20,7 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { int uncompressed = msg.readableBytes(); - if (uncompressed <= threshold) { + if (uncompressed < threshold) { // Under the threshold, there is nothing to do. ProtocolUtils.writeVarInt(out, 0); out.writeBytes(msg); @@ -54,4 +54,8 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder { public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { compressor.close(); } + + public void setThreshold(int threshold) { + this.threshold = threshold; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java index e423d04b2..c24f1e741 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java @@ -15,8 +15,8 @@ import io.netty.handler.codec.CorruptedFrameException; public class MinecraftDecoder extends ChannelInboundHandlerAdapter { public static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging"); - private static final QuietDecoderException DECODE_FAILED = - new QuietDecoderException("A packet did not decode successfully (invalid data). If you are a " + private static final QuietRuntimeException DECODE_FAILED = + new QuietRuntimeException("A packet did not decode successfully (invalid data). If you are a " + "developer, launch Velocity with -Dvelocity.packet-decode-logging=true to see more."); private final PacketDirection direction; @@ -64,8 +64,16 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { ctx.fireChannelRead(buf); } else { try { + doLengthSanityChecks(buf, packet); + + try { + packet.decode(buf, direction, registry.version); + } catch (Exception e) { + throw handleDecodeFailure(e, packet, packetId); + } + if (buf.isReadable()) { - throw handleNotReadEnough(packet, packetId); + throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex()); } ctx.fireChannelRead(packet); } finally { @@ -74,10 +82,30 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { } } - private Exception handleNotReadEnough(Packet packet, int packetId) { + private void doLengthSanityChecks(ByteBuf buf, Packet packet) throws Exception { + int expectedMinLen = packet.expectedMinLength(buf, direction, registry.version); + int expectedMaxLen = packet.expectedMaxLength(buf, direction, registry.version); + if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) { + throw handleOverflow(packet, expectedMaxLen, buf.readableBytes()); + } + if (buf.readableBytes() < expectedMinLen) { + throw handleUnderflow(packet, expectedMaxLen, buf.readableBytes()); + } + } + + private Exception handleOverflow(Packet packet, int expected, int actual) { if (DEBUG) { - return new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " " - + getExtraConnectionDetail(packetId)); + return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too " + + "big (expected " + expected + " bytes, got " + actual + " bytes)"); + } else { + return DECODE_FAILED; + } + } + + private Exception handleUnderflow(Packet packet, int expected, int actual) { + if (DEBUG) { + return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too " + + "small (expected " + expected + " bytes, got " + actual + " bytes)"); } else { return DECODE_FAILED; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/MinecraftPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/MinecraftPacket.java new file mode 100644 index 000000000..e7baaecb1 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/MinecraftPacket.java @@ -0,0 +1,24 @@ +package com.velocitypowered.proxy.protocol; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import io.netty.buffer.ByteBuf; + +public interface MinecraftPacket { + + void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion); + + void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion); + + boolean handle(MinecraftSessionHandler handler); + + default int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return -1; + } + + default int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java new file mode 100644 index 000000000..ecd610629 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java @@ -0,0 +1,129 @@ +package com.velocitypowered.proxy.protocol.packet; + +import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.transformLegacyToModernChannel; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class PluginMessage extends DeferredByteBufHolder implements MinecraftPacket { + + private @Nullable String channel; + + public PluginMessage() { + super(null); + } + + public PluginMessage(String channel, + @MonotonicNonNull ByteBuf backing) { + super(backing); + this.channel = channel; + } + + public String getChannel() { + if (channel == null) { + throw new IllegalStateException("Channel is not specified."); + } + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + @Override + public String toString() { + return "PluginMessage{" + + "channel='" + channel + '\'' + + ", data=" + super.toString() + + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + this.channel = ProtocolUtils.readString(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { + this.channel = transformLegacyToModernChannel(this.channel); + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.replace(buf.readRetainedSlice(buf.readableBytes())); + } else { + this.replace(ProtocolUtils.readRetainedByteBufSlice17(buf)); + } + + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + if (channel == null) { + throw new IllegalStateException("Channel is not specified."); + } + + if (refCnt() == 0) { + throw new IllegalStateException("Plugin message contents for " + this.channel + + " freed too many times."); + } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { + ProtocolUtils.writeString(buf, transformLegacyToModernChannel(this.channel)); + } else { + ProtocolUtils.writeString(buf, this.channel); + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + buf.writeBytes(content()); + } else { + ProtocolUtils.writeByteBuf17(content(), buf, true); // True for Forge support + } + + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + @Override + public PluginMessage copy() { + return (PluginMessage) super.copy(); + } + + @Override + public PluginMessage duplicate() { + return (PluginMessage) super.duplicate(); + } + + @Override + public PluginMessage retainedDuplicate() { + return (PluginMessage) super.retainedDuplicate(); + } + + @Override + public PluginMessage replace(ByteBuf content) { + return (PluginMessage) super.replace(content); + } + + @Override + public PluginMessage retain() { + return (PluginMessage) super.retain(); + } + + @Override + public PluginMessage retain(int increment) { + return (PluginMessage) super.retain(increment); + } + + @Override + public PluginMessage touch() { + return (PluginMessage) super.touch(); + } + + @Override + public PluginMessage touch(Object hint) { + return (PluginMessage) super.touch(hint); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java index 1bec0d119..4049be785 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java @@ -7,6 +7,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.scheduler.ScheduledTask; import com.velocitypowered.api.scheduler.Scheduler; @@ -183,8 +184,18 @@ public class VelocityScheduler implements Scheduler { currentTaskThread = Thread.currentThread(); try { runnable.run(); - } catch (Exception e) { - Log.logger.error("Exception in task {} by plugin {}", runnable, plugin, e); + } catch (Throwable e) { + //noinspection ConstantConditions + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } else { + String friendlyPluginName = pluginManager.fromInstance(plugin) + .map(container -> container.getDescription().getName() + .orElse(container.getDescription().getId())) + .orElse("UNKNOWN"); + Log.logger.error("Exception in task {} by plugin {}", runnable, friendlyPluginName, + e); + } } finally { if (repeat == 0) { onFinish(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index a15c286ab..94433a24c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -125,7 +125,9 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud } /** - * Sends a plugin message to the server through this connection. + * Sends a plugin message to the server through this connection. The message will be released + * afterwards. + * * @param identifier the channel ID to use * @param data the data * @return whether or not the message was sent @@ -133,11 +135,12 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud public boolean sendPluginMessage(ChannelIdentifier identifier, ByteBuf data) { for (ConnectedPlayer player : players.values()) { VelocityServerConnection connection = player.getConnectedServer(); - if (connection != null && connection.getServerInfo().equals(serverInfo)) { + if (connection != null && connection.getServer() == this) { return connection.sendPluginMessage(identifier, data); } } + data.release(); return false; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index e7d83c679..728ec5ad2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -21,16 +21,17 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class VelocityTabList implements TabList { + protected final ConnectedPlayer player; protected final MinecraftConnection connection; protected final Map entries = new ConcurrentHashMap<>(); - public VelocityTabList(MinecraftConnection connection) { - this.connection = connection; + public VelocityTabList(final ConnectedPlayer player) { + this.player = player; + this.connection = player.getConnection(); } @Override - public void setHeaderAndFooter(net.kyori.adventure.text.Component header, - net.kyori.adventure.text.Component footer) { + public void setHeaderAndFooter(Component header, Component footer) { Preconditions.checkNotNull(header, "header"); Preconditions.checkNotNull(footer, "footer"); GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer( diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/IdentityHashStrategy.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/IdentityHashStrategy.java new file mode 100644 index 000000000..b741d3ac5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/IdentityHashStrategy.java @@ -0,0 +1,24 @@ +package com.velocitypowered.proxy.util.collect; + +import it.unimi.dsi.fastutil.Hash.Strategy; + +public final class IdentityHashStrategy implements Strategy { + + @SuppressWarnings("rawtypes") + private static final IdentityHashStrategy INSTANCE = new IdentityHashStrategy(); + + public static Strategy instance() { + //noinspection unchecked + return INSTANCE; + } + + @Override + public int hashCode(T o) { + return System.identityHashCode(o); + } + + @Override + public boolean equals(T a, T b) { + return a == b; + } +} diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index c29cb1d01..fbad6626c 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -6,7 +6,7 @@ bind = "0.0.0.0:25577" # What should be the MOTD? This gets displayed when the player adds your server to # their server list. Legacy color codes and JSON are accepted. -motd = "&3A Velocity Server" +motd = " add3A Velocity Server" # What should we display for the maximum number of players? (Velocity does not support a cap # on the number of players online.) @@ -33,7 +33,7 @@ prevent-client-proxy-connections = false # Velocity's native forwarding. Only applicable for Minecraft 1.13 or higher. player-info-forwarding-mode = "NONE" -# If you are using modern or BungeeGuard IP forwarding, configure an unique secret here. +# If you are using modern or BungeeGuard IP forwarding, configure a unique secret here. forwarding-secret = "" # Announce whether or not your server supports Forge. If you run a modded server, we @@ -69,7 +69,7 @@ lobby = "127.0.0.1:30066" factions = "127.0.0.1:30067" minigames = "127.0.0.1:30068" -# In what order we should try servers when a player logs in or is kicked from aserver. +# In what order we should try servers when a player logs in or is kicked from a server. try = [ "lobby" ] @@ -105,8 +105,9 @@ connection-timeout = 5000 # Specify a read timeout for connections here. The default is 30 seconds. read-timeout = 30000 -# Enables compatibility with HAProxy. -proxy-protocol = false +# Enables compatibility with HAProxy's PROXY protocol. If you don't know what this is for, then +# don't enable it. +haproxy-protocol = false # Enables TCP fast open support on the proxy. Requires the proxy to run on Linux. tcp-fast-open = false @@ -142,19 +143,6 @@ map = "Velocity" # Whether plugins should be shown in query response by default or not show-plugins = false -[metrics] -# Whether metrics will be reported to bStats (https://bstats.org). -# bStats collects some basic information, like how many people use Velocity and their -# player count. We recommend keeping bStats enabled, but if you're not comfortable with -# this, you can turn this setting off. There is no performance penalty associated with -# having metrics enabled, and data sent to bStats can't identify your server. -enabled = true - -# A unique, anonymous ID to identify this proxy with. -id = "" - -log-failure = false - # Legacy color codes and JSON are accepted in all messages. [messages] # Prefix when the player gets kicked from a server. @@ -169,4 +157,4 @@ online-mode-only = "&cThis server only accepts connections from online-mode clie no-available-servers = "&cThere are no available servers." already-connected = "&cYou are already connected to this proxy!" moved-to-new-server-prefix = "&cThe server you were on kicked you: " -generic-connection-error = "&cAn internal error occurred in your connection." \ No newline at end of file +generic-connection-error = "&cAn internal error occurred in your connection." diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandManagerTests.java b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandManagerTests.java index 7f964d1c8..fe77b362c 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandManagerTests.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandManagerTests.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.command.SimpleCommand; import com.velocitypowered.proxy.event.MockEventManager; import com.velocitypowered.proxy.event.VelocityEventManager; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; @@ -423,6 +424,54 @@ public class CommandManagerTests { manager.offerSuggestions(MockCommandSource.INSTANCE, "foo2 baz ").join()); } + @Test + void testSuggestionPermissions() throws ExecutionException, InterruptedException { + VelocityCommandManager manager = createManager(); + RawCommand rawCommand = new RawCommand() { + @Override + public void execute(final Invocation invocation) { + fail("The Command should not be executed while testing suggestions"); + } + + @Override + public boolean hasPermission(Invocation invocation) { + return invocation.arguments().length() > 0; + } + + @Override + public List suggest(final Invocation invocation) { + return ImmutableList.of("suggestion"); + } + }; + + manager.register(rawCommand, "foo"); + + assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo").get().isEmpty()); + assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo bar").get().isEmpty()); + + Command oldCommand = new Command() { + @Override + public void execute(CommandSource source, String @NonNull [] args) { + fail("The Command should not be executed while testing suggestions"); + } + + @Override + public boolean hasPermission(CommandSource source, String @NonNull [] args) { + return args.length > 0; + } + + @Override + public List suggest(CommandSource source, String @NonNull [] currentArgs) { + return ImmutableList.of("suggestion"); + } + }; + + manager.register(oldCommand, "bar"); + + assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar").get().isEmpty()); + assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar foo").get().isEmpty()); + } + static class NoopSimpleCommand implements SimpleCommand { @Override public void execute(final Invocation invocation) {