diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c0ad9c30..07f49bf1 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -14,10 +14,10 @@ jobs: persist-credentials: false - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: 17 - distribution: 'temurin' + java-version: 21 + distribution: 'zulu' - name: Build with Gradle run: ./gradlew build diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 65234396..739945b5 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -61,6 +61,7 @@ tasks { o.encoding = "UTF-8" o.source = "17" + o.use() o.links( "https://www.slf4j.org/apidocs/", "https://guava.dev/releases/${libs.guava.get().version}/api/docs/", diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index ad389373..c36ca927 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -7,8 +7,10 @@ package com.velocitypowered.api.command; +import com.mojang.brigadier.suggestion.Suggestions; import com.velocitypowered.api.event.command.CommandExecuteEvent; import java.util.Collection; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import org.checkerframework.checker.nullness.qual.Nullable; @@ -116,6 +118,27 @@ public interface CommandManager { */ CompletableFuture executeImmediatelyAsync(CommandSource source, String cmdLine); + /** + * Asynchronously collects suggestions to fill in the given command {@code cmdLine}. + * Returns only the raw completion suggestions without tooltips. + * + * @param source the source to execute the command for + * @param cmdLine the partially completed command + * @return a {@link CompletableFuture} eventually completed with a {@link List}, possibly empty + */ + CompletableFuture> offerSuggestions(CommandSource source, String cmdLine); + + /** + * Asynchronously collects suggestions to fill in the given command {@code cmdLine}. + * Returns the brigadier {@link Suggestions} with tooltips for each result. + * + * @param source the source to execute the command for + * @param cmdLine the partially completed command + * @return a {@link CompletableFuture} eventually completed with {@link Suggestions}, possibly + * empty + */ + CompletableFuture offerBrigadierSuggestions(CommandSource source, String cmdLine); + /** * Returns an immutable collection of the case-insensitive aliases registered * on this manager. diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java index b3263639..3cc03925 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java @@ -27,7 +27,7 @@ public interface CommandSource extends Audience, PermissionSubject { * for more information on the format. **/ default void sendRichMessage(final @NotNull String message) { - this.sendMessage(MiniMessage.miniMessage().deserialize(message)); + this.sendMessage(MiniMessage.miniMessage().deserialize(message, this)); } /** @@ -43,7 +43,7 @@ public interface CommandSource extends Audience, PermissionSubject { final @NotNull String message, final @NotNull TagResolver @NotNull... resolvers ) { - this.sendMessage(MiniMessage.miniMessage().deserialize(message, resolvers)); + this.sendMessage(MiniMessage.miniMessage().deserialize(message, this, resolvers)); } /** diff --git a/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java b/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java index 3f9b0298..d00cffac 100644 --- a/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java @@ -9,7 +9,6 @@ package com.velocitypowered.api.event.command; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.annotations.Beta; import com.mojang.brigadier.tree.RootCommandNode; import com.velocitypowered.api.event.annotation.AwaitingEvent; import com.velocitypowered.api.proxy.Player; @@ -21,7 +20,6 @@ import com.velocitypowered.api.proxy.Player; * client. */ @AwaitingEvent -@Beta public class PlayerAvailableCommandsEvent { private final Player player; diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java index 1be39544..95e70a49 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java @@ -7,7 +7,6 @@ package com.velocitypowered.api.event.player; -import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -18,7 +17,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; * available in {@link Player#getCurrentServer()}. Velocity will not wait on this event to finish * firing. */ -@Beta public class ServerPostConnectEvent { private final Player player; private final RegisteredServer previousServer; diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java index 754492a6..1ffb58bc 100644 --- a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java @@ -7,7 +7,6 @@ package com.velocitypowered.api.event.proxy.server; -import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; @@ -23,7 +22,6 @@ import org.jetbrains.annotations.NotNull; * @param registeredServer A {@link RegisteredServer} that has been registered. * @since 3.3.0 */ -@Beta public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) { public ServerRegisteredEvent { Preconditions.checkNotNull(registeredServer, "registeredServer"); diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java index 36b4023b..1d9548b7 100644 --- a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java @@ -7,7 +7,6 @@ package com.velocitypowered.api.event.proxy.server; -import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; @@ -23,7 +22,6 @@ import org.jetbrains.annotations.NotNull; * @param unregisteredServer A {@link RegisteredServer} that has been unregistered. * @since 3.3.0 */ -@Beta public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) { public ServerUnregisteredEvent { Preconditions.checkNotNull(unregisteredServer, "unregisteredServer"); diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index d1eaa389..039fec40 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -89,7 +89,10 @@ public enum ProtocolVersion implements Ordered { MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), MINECRAFT_1_21(767, "1.21", "1.21.1"), MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"), - MINECRAFT_1_21_4(769, "1.21.4"); + MINECRAFT_1_21_4(769, "1.21.4"), + MINECRAFT_1_21_5(770, "1.21.5"), + MINECRAFT_1_21_6(771, "1.21.6"), + MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"); private static final int SNAPSHOT_BIT = 30; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index f043b0c6..bbf999b2 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -41,6 +41,13 @@ public interface ProxyServer extends Audience { */ void shutdown(); + /** + * Returns whether the proxy is currently shutting down. + * + * @return {@code true} if the proxy is shutting down, {@code false} otherwise + */ + boolean isShuttingDown(); + /** * Closes all listening endpoints for this server. * This includes the main minecraft listener and query channel. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java index 34cbfef3..2510dae4 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java @@ -149,6 +149,61 @@ public interface ProxyConfig { */ int getReadTimeout(); + /** + * Get the rate limit for how fast a player can execute commands. + * + * @return the command rate limit (in milliseconds) + */ + int getCommandRatelimit(); + + /** + * Get whether we should forward commands to the backend if the player is rate limited. + * + * @return whether to forward commands if rate limited + */ + boolean isForwardCommandsIfRateLimited(); + + /** + * Get the kick limit for commands that are rate limited. + * If this limit is 0 or less, the player will be not be kicked. + * + * @return the rate limited command rate limit + */ + int getKickAfterRateLimitedCommands(); + + /** + * Get whether the proxy should kick players who are command rate limited. + * + * @return whether to kick players who are rate limited + */ + default boolean isKickOnCommandRateLimit() { + return getKickAfterRateLimitedCommands() > 0; + } + + /** + * Get the rate limit for how fast a player can tab complete. + * + * @return the tab complete rate limit (in milliseconds) + */ + int getTabCompleteRatelimit(); + + /** + * Get the kick limit for tab completes that are rate limited. + * If this limit is 0 or less, the player will be not be kicked. + * + * @return the rate limited command rate limit + */ + int getKickAfterRateLimitedTabCompletes(); + + /** + * Get whether the proxy should kick players who are tab complete rate limited. + * + * @return whether to kick players who are rate limited + */ + default boolean isKickOnTabCompleteRateLimit() { + return getKickAfterRateLimitedTabCompletes() > 0; + } + String getNeteaseAuthUrl(); String getNeteaseGameId(); 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 98967cfd..a271a414 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 @@ -11,7 +11,6 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Strings; import java.util.Objects; -import java.util.regex.Pattern; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; @@ -21,8 +20,6 @@ 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 final String namespace; private final String name; @@ -39,7 +36,7 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { * @return a new channel identifier */ public static MinecraftChannelIdentifier forDefaultNamespace(String name) { - return new MinecraftChannelIdentifier("minecraft", name); + return new MinecraftChannelIdentifier(Key.MINECRAFT_NAMESPACE, name); } /** @@ -52,14 +49,10 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { public static MinecraftChannelIdentifier create(String namespace, String name) { checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty"); checkArgument(name != null, "namespace is null or empty"); - checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(), - "namespace is not valid, must match: %s got %s", - VALID_IDENTIFIER_REGEX.toString(), - namespace); - checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(), - "name is not valid, must match: %s got %s", - VALID_IDENTIFIER_REGEX.toString(), - name); + checkArgument(Key.parseableNamespace(namespace), + "namespace is not valid, must match: [a-z0-9_.-] got %s", namespace); + checkArgument(Key.parseableValue(name), + "name is not valid, must match: [a-z0-9/._-] got %s", name); return new MinecraftChannelIdentifier(namespace, name); } @@ -72,10 +65,9 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { public static MinecraftChannelIdentifier from(String identifier) { int colonPos = identifier.indexOf(':'); if (colonPos == -1) { - throw new IllegalArgumentException("Identifier does not contain a colon."); - } - if (colonPos + 1 == identifier.length()) { - throw new IllegalArgumentException("Identifier is empty."); + return create(Key.MINECRAFT_NAMESPACE, identifier); + } else if (colonPos == 0) { + return create(Key.MINECRAFT_NAMESPACE, identifier.substring(1)); } String namespace = identifier.substring(0, colonPos); String name = identifier.substring(colonPos + 1); diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java index feffd76e..a4035530 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -40,6 +40,7 @@ public interface TabList { * Adds a {@link TabListEntry} to the {@link Player}'s tab list. * * @param entry to add to the tab list + * @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists */ void addEntry(TabListEntry entry); @@ -47,6 +48,7 @@ public interface TabList { * Adds a {@link Iterable} of {@link TabListEntry}'s to the {@link Player}'s tab list. * * @param entries to add to the tab list + * @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists */ default void addEntries(Iterable entries) { for (TabListEntry entry : entries) { @@ -58,6 +60,7 @@ public interface TabList { * Adds an array of {@link TabListEntry}'s to the {@link Player}'s tab list. * * @param entries to add to the tab list + * @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists */ default void addEntries(TabListEntry... entries) { for (TabListEntry entry : entries) { @@ -187,6 +190,26 @@ public interface TabList { * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. */ @Deprecated + default TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, + int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder) { + return buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, true); + } + + /** + * Represents an entry in a {@link Player}'s tab list. + * + * @param profile the profile + * @param displayName the display name + * @param latency the latency + * @param gameMode the game mode + * @param chatSession the chat session + * @param listed the visible status of entry + * @param listOrder the order/priority of entry in the tab list + * @param showHat the visibility of this entry's hat layer + * @return the entry + * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. + */ + @Deprecated TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, - int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder); + int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java index 350dc896..aea45287 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java @@ -160,6 +160,27 @@ public interface TabListEntry extends KeyIdentifiable { return this; } + /** + * Returns whether this entry's hat layer is shown in the tab list. + * + * @return whether to show this entry's hat layer + * @sinceMinecraft 1.21.4 + */ + default boolean isShowHat() { + return true; + } + + /** + * Sets whether to show this entry's hat layer in the tab list. + * + * @param showHat whether to show this entry's hat layer + * @return {@code this}, for chaining + * @sinceMinecraft 1.21.4 + */ + default TabListEntry setShowHat(boolean showHat) { + return this; + } + /** * Returns a {@link Builder} to create a {@link TabListEntry}. * @@ -183,6 +204,7 @@ public interface TabListEntry extends KeyIdentifiable { private int gameMode = 0; private boolean listed = true; private int listOrder = 0; + private boolean showHat; private @Nullable ChatSession chatSession; @@ -268,7 +290,7 @@ public interface TabListEntry extends KeyIdentifiable { * Sets whether this entry should be visible. * * @param listed to set - * @return ${code this}, for chaining + * @return {@code this}, for chaining * @see TabListEntry#isListed() */ public Builder listed(boolean listed) { @@ -280,7 +302,7 @@ public interface TabListEntry extends KeyIdentifiable { * Sets the order/priority of this entry in the tab list. * * @param order to set - * @return ${code this}, for chaining + * @return {@code this}, for chaining * @sinceMinecraft 1.21.2 * @see TabListEntry#getListOrder() */ @@ -289,6 +311,18 @@ public interface TabListEntry extends KeyIdentifiable { return this; } + /** + * Sets whether this entry's hat layer should be shown in the tab list. + * + * @param showHat to set + * @return {@code this}, for chaining + * @see TabListEntry#isShowHat() + */ + public Builder showHat(boolean showHat) { + this.showHat = showHat; + return this; + } + /** * Constructs the {@link TabListEntry} specified by {@code this} {@link Builder}. * @@ -301,7 +335,7 @@ public interface TabListEntry extends KeyIdentifiable { if (profile == null) { throw new IllegalStateException("The GameProfile must be set when building a TabListEntry"); } - return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder); + return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, showHat); } } } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java b/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java index 51be358e..a3cd0ac1 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java @@ -15,6 +15,7 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; import net.kyori.adventure.builder.AbstractBuilder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Contains the parameters used to ping a {@link RegisteredServer}. @@ -30,10 +31,12 @@ public final class PingOptions { public static final PingOptions DEFAULT = PingOptions.builder().build(); private final ProtocolVersion protocolVersion; private final long timeout; + private final String virtualHost; private PingOptions(final Builder builder) { this.protocolVersion = builder.protocolVersion; this.timeout = builder.timeout; + this.virtualHost = builder.virtualHost; } /** @@ -54,6 +57,16 @@ public final class PingOptions { return this.timeout; } + /** + * The virtual host to pass to the server for the ping. + * + * @return the virtual hostname to pass to the server for the ping + * @since 3.4.0 + */ + public @Nullable String getVirtualHost() { + return this.virtualHost; + } + /** * Create a new builder to assign values to a new PingOptions. * @@ -68,10 +81,9 @@ public final class PingOptions { if (o == null) { return false; } - if (!(o instanceof PingOptions)) { + if (!(o instanceof final PingOptions other)) { return false; } - final PingOptions other = (PingOptions) o; return Objects.equals(this.protocolVersion, other.protocolVersion) && Objects.equals(this.timeout, other.timeout); } @@ -97,6 +109,7 @@ public final class PingOptions { public static final class Builder implements AbstractBuilder { private ProtocolVersion protocolVersion = ProtocolVersion.UNKNOWN; private long timeout = 0; + private String virtualHost = null; private Builder() { } @@ -146,6 +159,18 @@ public final class PingOptions { return this; } + /** + * Sets the virtual host to pass to the server for the ping. + * + * @param virtualHost the virtual hostname to pass to the server for the ping + * @return this builder + * @since 3.4.0 + */ + public Builder virtualHost(final @Nullable String virtualHost) { + this.virtualHost = virtualHost; + return this; + } + /** * Create a new {@link PingOptions} with the values of this Builder. * 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 a7d9518f..4d906b7b 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 @@ -14,6 +14,7 @@ import com.velocitypowered.api.util.Favicon; import com.velocitypowered.api.util.ModInfo; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -159,31 +160,79 @@ public final class ServerPing { } + /** + * Uses the modified {@code version} info in the response. + * + * @param version version info to set + * @return this builder, for chaining + */ public Builder version(Version version) { this.version = Preconditions.checkNotNull(version, "version"); return this; } + /** + * Uses the modified {@code onlinePlayers} number in the response. + * + * @param onlinePlayers number for online players to set + * @return this builder, for chaining + */ public Builder onlinePlayers(int onlinePlayers) { this.onlinePlayers = onlinePlayers; return this; } + /** + * Uses the modified {@code maximumPlayers} number in the response. + * This will not modify the actual maximum players that can join the server. + * + * @param maximumPlayers number for maximum players to set + * @return this builder, for chaining + */ public Builder maximumPlayers(int maximumPlayers) { this.maximumPlayers = maximumPlayers; return this; } + /** + * Uses the modified {@code players} array in the response. + * + * @param players array of SamplePlayers to add + * @return this builder, for chaining + */ public Builder samplePlayers(SamplePlayer... players) { this.samplePlayers.addAll(Arrays.asList(players)); return this; } + /** + * Uses the modified {@code players} collection in the response. + * + * @param players collection of SamplePlayers to add + * @return this builder, for chaining + */ + public Builder samplePlayers(Collection players) { + this.samplePlayers.addAll(players); + return this; + } + + /** + * Uses the modified {@code modType} in the response. + * + * @param modType the mod type to set + * @return this builder, for chaining + */ public Builder modType(String modType) { this.modType = Preconditions.checkNotNull(modType, "modType"); return this; } + /** + * Uses the modified {@code mods} array in the response. + * + * @param mods array of mods to use + * @return this builder, for chaining + */ public Builder mods(ModInfo.Mod... mods) { this.mods.addAll(Arrays.asList(mods)); return this; @@ -193,7 +242,7 @@ public final class ServerPing { * Uses the modified {@code mods} list in the response. * * @param mods the mods list to use - * @return this build, for chaining + * @return this builder, for chaining */ public Builder mods(ModInfo mods) { Preconditions.checkNotNull(mods, "mods"); @@ -203,36 +252,74 @@ public final class ServerPing { return this; } + /** + * Clears the current list of mods to use in the response. + * + * @return this builder, for chaining + */ public Builder clearMods() { this.mods.clear(); return this; } + /** + * Clears the current list of PlayerSamples to use in the response. + * + * @return this builder, for chaining + */ public Builder clearSamplePlayers() { this.samplePlayers.clear(); return this; } + /** + * Defines the server as mod incompatible in the response. + * + * @return this builder, for chaining + */ public Builder notModCompatible() { this.nullOutModinfo = true; return this; } + /** + * Enables nulling Players in the response. + * This will display the player count as {@code ???}. + * + * @return this builder, for chaining + */ public Builder nullPlayers() { this.nullOutPlayers = true; return this; } + /** + * Uses the {@code description} Component in the response. + * + * @param description Component to use as the description. + * @return this builder, for chaining + */ public Builder description(net.kyori.adventure.text.Component description) { this.description = Preconditions.checkNotNull(description, "description"); return this; } + /** + * Uses the {@code favicon} in the response. + * + * @param favicon Favicon instance to use. + * @return this builder, for chaining + */ public Builder favicon(Favicon favicon) { this.favicon = Preconditions.checkNotNull(favicon, "favicon"); return this; } + /** + * Clears the current favicon used in the response. + * + * @return this builder, for chaining + */ public Builder clearFavicon() { this.favicon = null; return this; @@ -429,6 +516,10 @@ public final class ServerPing { */ public static final class SamplePlayer { + public static final SamplePlayer ANONYMOUS = new SamplePlayer( + "Anonymous Player", + new UUID(0L, 0L) + ); private final String name; private final UUID id; diff --git a/api/src/main/java/com/velocitypowered/api/util/ModInfo.java b/api/src/main/java/com/velocitypowered/api/util/ModInfo.java index 8a51f322..cfc52289 100644 --- a/api/src/main/java/com/velocitypowered/api/util/ModInfo.java +++ b/api/src/main/java/com/velocitypowered/api/util/ModInfo.java @@ -76,9 +76,17 @@ public final class ModInfo { private final String id; private final String version; + /** + * Creates a new mod info. + * + * @param id the mod identifier + * @param version the mod version + */ public Mod(String id, String version) { this.id = Preconditions.checkNotNull(id, "id"); this.version = Preconditions.checkNotNull(version, "version"); + Preconditions.checkArgument(id.length() < 128, "mod id is too long"); + Preconditions.checkArgument(version.length() < 128, "mod version is too long"); } public String getId() { 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 4c8919f3..9be12fc5 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 @@ -47,17 +47,25 @@ class MinecraftChannelIdentifierTest { create("velocity", "test/test2"); } + @Test + void fromIdentifierDefaultNamespace() { + assertEquals("minecraft", from("test").getNamespace()); + assertEquals("minecraft", from(":test").getNamespace()); + } + + @Test + void fromIdentifierAllowsEmptyName() { + from("minecraft:"); + from(":"); + from(""); + } + @Test void fromIdentifierThrowsOnBadValues() { assertAll( - () -> assertThrows(IllegalArgumentException.class, () -> from("")), - () -> assertThrows(IllegalArgumentException.class, () -> from(":")), - () -> assertThrows(IllegalArgumentException.class, () -> from(":a")), - () -> assertThrows(IllegalArgumentException.class, () -> from("a:")), () -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")), + () -> assertThrows(IllegalArgumentException.class, () -> from("he/llo:wor/ld")), () -> assertThrows(IllegalArgumentException.class, () -> from("hello::")) ); } - - -} \ No newline at end of file +} diff --git a/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts b/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts index 1901430f..e38ef34d 100644 --- a/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts +++ b/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts @@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar import org.gradle.kotlin.dsl.withType import java.io.ByteArrayOutputStream +// This interface is needed as a workaround to get an instance of ExecOperations +interface Injected { + @get:Inject + val execOps: ExecOperations +} + val currentShortRevision = ByteArrayOutputStream().use { - exec { + val execOps = objects.newInstance().execOps + execOps.exec { executable = "git" args = listOf("rev-parse", "HEAD") standardOutput = it diff --git a/build-logic/src/main/kotlin/velocity-publish.gradle.kts b/build-logic/src/main/kotlin/velocity-publish.gradle.kts index 51de5c67..a7cc744a 100644 --- a/build-logic/src/main/kotlin/velocity-publish.gradle.kts +++ b/build-logic/src/main/kotlin/velocity-publish.gradle.kts @@ -8,7 +8,7 @@ extensions.configure { maven { credentials(PasswordCredentials::class.java) - name = "paper" + name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined val base = "https://repo.papermc.io/repository/maven" val releasesRepoUrl = "$base-releases/" val snapshotsRepoUrl = "$base-snapshots/" diff --git a/build.gradle.kts b/build.gradle.kts index 9e862dad..a761de5f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,11 +20,11 @@ subprojects { testImplementation(rootProject.libs.junit) } - tasks { - test { - useJUnitPlatform() - reports { - junitXml.required.set(true) + testing.suites.named("test") { + useJUnitJupiter() + targets.all { + testTask.configure { + reports.junitXml.required = true } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e3859eb0..fb1e863f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,23 +2,24 @@ configurate3 = "3.7.3" configurate4 = "4.1.2" flare = "2.0.1" -log4j = "2.24.1" -netty = "4.1.114.Final" +log4j = "2.24.3" +netty = "4.2.1.Final" [plugins] +fill = "io.papermc.fill.gradle:1.0.3" indra-publishing = "net.kyori.indra.publishing:2.0.6" -shadow = "io.github.goooler.shadow:8.1.5" +shadow = "com.gradleup.shadow:8.3.6" spotless = "com.diffplug.spotless:6.25.0" [libraries] -adventure-bom = "net.kyori:adventure-bom:4.18.0" -adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.18.0" +adventure-bom = "net.kyori:adventure-bom:4.23.0" +adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.23.0" adventure-facet = "net.kyori:adventure-platform-facet:4.3.4" -asm = "org.ow2.asm:asm:9.6" +asm = "org.ow2.asm:asm:9.8" auto-service = "com.google.auto.service:auto-service:1.0.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" -bstats = "org.bstats:bstats-base:3.0.2" +bstats = "org.bstats:bstats-base:3.0.3" caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8" checker-qual = "org.checkerframework:checker-qual:3.42.0" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" @@ -33,11 +34,11 @@ disruptor = "com.lmax:disruptor:4.0.0" fastutil = "it.unimi.dsi:fastutil:8.5.15" flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } -jline = "org.jline:jline-terminal-jansi:3.27.1" +jline = "org.jline:jline-terminal-jansi:3.30.2" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" junit = "org.junit.jupiter:junit-jupiter:5.10.2" jspecify = "org.jspecify:jspecify:0.3.0" -kyori-ansi = "net.kyori:ansi:1.1.0" +kyori-ansi = "net.kyori:ansi:1.1.1" guava = "com.google.guava:guava:25.1-jre" gson = "com.google.code.gson:gson:2.10.1" guice = "com.google.inject:guice:6.0.0" @@ -54,8 +55,9 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } +netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" } nightconfig = "com.electronwill.night-config:toml:3.6.7" -slf4j = "org.slf4j:slf4j-api:2.0.12" +slf4j = "org.slf4j:slf4j-api:2.0.17" snakeyaml = "org.yaml:snakeyaml:1.33" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c82..ff23a68d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index ab736e20..1559f5fd 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -1,9 +1,11 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer +import io.papermc.fill.model.BuildChannel plugins { application id("velocity-init-manifest") alias(libs.plugins.shadow) + alias(libs.plugins.fill) } application { @@ -100,6 +102,7 @@ tasks { runShadow { workingDir = file("run").also(File::mkdirs) standardInput = System.`in` + jvmArgs("-Dvelocity.packet-decode-logging=true") } named("run") { workingDir = file("run").also(File::mkdirs) @@ -107,6 +110,24 @@ tasks { } } +val projectVersion = version as String +fill { + project("velocity") + + build { + channel = BuildChannel.STABLE + versionFamily("3.0.0") + version(projectVersion) + + downloads { + register("server:default") { + file = tasks.shadowJar.flatMap { it.archiveFile } + nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" } + } + } + } +} + dependencies { implementation(project(":velocity-api")) implementation(project(":velocity-native")) @@ -121,6 +142,9 @@ dependencies { implementation(libs.netty.transport.native.epoll) implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-x86_64") }) implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-aarch_64") }) + implementation(libs.netty.transport.native.iouring) + implementation(variantOf(libs.netty.transport.native.iouring) { classifier("linux-x86_64") }) + implementation(variantOf(libs.netty.transport.native.iouring) { classifier("linux-aarch_64") }) implementation(libs.netty.transport.native.kqueue) implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-x86_64") }) implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-aarch_64") }) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java index 89f63b56..7feeb25f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java @@ -65,7 +65,8 @@ public class Metrics { logger::info, config.isLogErrorsEnabled(), config.isLogSentDataEnabled(), - config.isLogResponseStatusTextEnabled() + config.isLogResponseStatusTextEnabled(), + false ); if (!config.didExistBefore()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java index 0ec8622d..a6fc850b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java @@ -47,6 +47,11 @@ public class Velocity { System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir")); } + // Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator + if (System.getProperty("io.netty.allocator.type") == null) { + System.setProperty("io.netty.allocator.type", "pooled"); + } + // Disable the resource leak detector by default as it reduces performance. Allow the user to // override this if desired. if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 45c228b3..c3e8d472 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -75,11 +75,13 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import java.io.IOException; import java.io.InputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.http.HttpClient; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyPair; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -103,7 +105,7 @@ import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.translation.GlobalTranslator; -import net.kyori.adventure.translation.TranslationRegistry; +import net.kyori.adventure.translation.TranslationStore; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bstats.MetricsBase; @@ -162,7 +164,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { private final Map connectionsByUuid = new ConcurrentHashMap<>(); private final Map connectionsByName = new ConcurrentHashMap<>(); private final VelocityConsole console; - private @MonotonicNonNull Ratelimiter ipAttemptLimiter; + private @MonotonicNonNull Ratelimiter ipAttemptLimiter; + private @MonotonicNonNull Ratelimiter commandRateLimiter; + private @MonotonicNonNull Ratelimiter tabCompleteRateLimiter; private final VelocityEventManager eventManager; private final VelocityScheduler scheduler; private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar(); @@ -295,6 +299,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { } ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit()); + commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit()); + tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit()); loadPlugins(); // Go ahead and fire the proxy initialization event. We block since plugins should have a chance @@ -332,8 +338,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { } private void registerTranslations() { - final TranslationRegistry translationRegistry = TranslationRegistry - .create(Key.key("velocity", "translations")); + final TranslationStore.StringBased translationRegistry = + TranslationStore.messageFormat(Key.key("velocity", "translations")); translationRegistry.defaultLocale(Locale.US); try { ResourceUtils.visitResources(VelocityServer.class, path -> { @@ -654,10 +660,18 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { return cm.createHttpClient(); } - public Ratelimiter getIpAttemptLimiter() { + public @MonotonicNonNull Ratelimiter getIpAttemptLimiter() { return ipAttemptLimiter; } + public @MonotonicNonNull Ratelimiter getCommandRateLimiter() { + return commandRateLimiter; + } + + public @MonotonicNonNull Ratelimiter getTabCompleteRateLimiter() { + return tabCompleteRateLimiter; + } + /** * Checks if the {@code connection} can be registered with the proxy. * @@ -803,6 +817,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { public VelocityChannelRegistrar getChannelRegistrar() { return channelRegistrar; } + + @Override + public boolean isShuttingDown() { + return shutdownInProgress.get(); + } @Override public InetSocketAddress getBoundAddress() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index b4b0e850..21f82f34 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -300,27 +300,14 @@ public class VelocityCommandManager implements CommandManager { ); } - /** - * Returns suggestions to fill in the given command. - * - * @param source the source to execute the command for - * @param cmdLine the partially completed command - * @return a {@link CompletableFuture} eventually completed with a {@link List}, possibly empty - */ + @Override public CompletableFuture> offerSuggestions(final CommandSource source, final String cmdLine) { return offerBrigadierSuggestions(source, cmdLine) .thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText)); } - /** - * Returns suggestions to fill in the given command. - * - * @param source the source to execute the command for - * @param cmdLine the partially completed command - * @return a {@link CompletableFuture} eventually completed with {@link Suggestions}, possibly - * empty - */ + @Override public CompletableFuture offerBrigadierSuggestions( final CommandSource source, final String cmdLine) { Preconditions.checkNotNull(source, "source"); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java index 402a0ce8..b60ead91 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.command.builtin; +import com.google.gson.JsonSyntaxException; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; @@ -25,8 +26,9 @@ import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.proxy.VelocityServer; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; /** * Shuts down the proxy. @@ -53,11 +55,22 @@ public final class ShutdownCommand { StringArgumentType.greedyString()) .executes(context -> { String reason = context.getArgument("reason", String.class); - server.shutdown(true, MiniMessage.miniMessage().deserialize( - MiniMessage.miniMessage().serialize( - LegacyComponentSerializer.legacy('&').deserialize(reason) - ) - )); + Component reasonComponent = null; + + if (reason.startsWith("{") || reason.startsWith("[") || reason.startsWith("\"")) { + try { + reasonComponent = GsonComponentSerializer.gson() + .deserializeOrNull(reason); + } catch (JsonSyntaxException expected) { + + } + } + + if (reasonComponent == null) { + reasonComponent = MiniMessage.miniMessage().deserialize(reason); + } + + server.shutdown(true, reasonComponent); return Command.SINGLE_SUCCESS; }) ).build()); 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 85bf2061..1e393476 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 @@ -82,7 +82,7 @@ public final class VelocityCommand { .executes(new Heap()) .build(); final LiteralCommandNode info = BrigadierCommand.literalArgumentBuilder("info") - .requires(source -> source.getPermissionValue("velocity.command.info") != Tristate.FALSE) + .requires(source -> source.getPermissionValue("velocity.command.info") == Tristate.TRUE) .executes(new Info(server)) .build(); final LiteralCommandNode plugins = BrigadierCommand 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 0f33559f..291a5e8b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -78,6 +78,8 @@ public class VelocityConfiguration implements ProxyConfig { private boolean onlineModeKickExistingPlayers = false; @Expose private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; + @Expose + private boolean samplePlayersInPing = false; private final Servers servers; private final ForcedHosts forcedHosts; @Expose @@ -110,9 +112,9 @@ public class VelocityConfiguration implements ProxyConfig { boolean preventClientProxyConnections, boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, - boolean enablePlayerAddressLogging, Servers servers, ForcedHosts forcedHosts, - Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication, - String authUrl, String gameId) { + boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers, + ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics, + boolean forceKeyAuthentication, String authUrl, String gameId) { this.bind = bind; this.motd = motd; this.showMaxPlayers = showMaxPlayers; @@ -123,6 +125,7 @@ public class VelocityConfiguration implements ProxyConfig { this.forwardingSecret = forwardingSecret; this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers; this.pingPassthrough = pingPassthrough; + this.samplePlayersInPing = samplePlayersInPing; this.enablePlayerAddressLogging = enablePlayerAddressLogging; this.servers = servers; this.forcedHosts = forcedHosts; @@ -238,6 +241,11 @@ public class VelocityConfiguration implements ProxyConfig { valid = false; } + if (advanced.commandRateLimit < 0) { + logger.error("Invalid command rate limit {}", advanced.commandRateLimit); + valid = false; + } + loadFavicon(); return valid; @@ -359,6 +367,31 @@ public class VelocityConfiguration implements ProxyConfig { return advanced.getReadTimeout(); } + @Override + public int getCommandRatelimit() { + return advanced.getCommandRateLimit(); + } + + @Override + public int getTabCompleteRatelimit() { + return advanced.getTabCompleteRateLimit(); + } + + @Override + public int getKickAfterRateLimitedTabCompletes() { + return advanced.getKickAfterRateLimitedTabCompletes(); + } + + @Override + public boolean isForwardCommandsIfRateLimited() { + return advanced.isForwardCommandsIfRateLimited(); + } + + @Override + public int getKickAfterRateLimitedCommands() { + return advanced.getKickAfterRateLimitedCommands(); + } + public boolean isProxyProtocol() { return advanced.isProxyProtocol(); } @@ -379,6 +412,10 @@ public class VelocityConfiguration implements ProxyConfig { return pingPassthrough; } + public boolean getSamplePlayersInPing() { + return samplePlayersInPing; + } + public boolean isPlayerAddressLoggingEnabled() { return enablePlayerAddressLogging; } @@ -415,6 +452,10 @@ public class VelocityConfiguration implements ProxyConfig { return forceKeyAuthentication; } + public boolean isEnableReusePort() { + return advanced.isEnableReusePort(); + } + @Override public String getNeteaseAuthUrl() { return authUrl; @@ -523,6 +564,8 @@ public class VelocityConfiguration implements ProxyConfig { final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", PingPassthroughMode.DISABLED); + final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false); + final String bind = config.getOrElse("bind", "0.0.0.0:25565"); final int maxPlayers = config.getIntOrElse("show-max-players", 500); final boolean onlineMode = config.getOrElse("online-mode", true); @@ -555,6 +598,7 @@ public class VelocityConfiguration implements ProxyConfig { forwardingSecret, kickExisting, pingPassthroughMode, + samplePlayersInPing, enablePlayerAddressLogging, new Servers(serversConfig), new ForcedHosts(forcedHostsConfig), @@ -740,6 +784,18 @@ public class VelocityConfiguration implements ProxyConfig { private boolean logPlayerConnections = true; @Expose private boolean acceptTransfers = false; + @Expose + private boolean enableReusePort = false; + @Expose + private int commandRateLimit = 50; + @Expose + private boolean forwardCommandsIfRateLimited = true; + @Expose + private int kickAfterRateLimitedCommands = 5; + @Expose + private int tabCompleteRateLimit = 50; + @Expose + private int kickAfterRateLimitedTabCompletes = 10; private Advanced() { } @@ -765,6 +821,12 @@ public class VelocityConfiguration implements ProxyConfig { this.logCommandExecutions = config.getOrElse("log-command-executions", false); this.logPlayerConnections = config.getOrElse("log-player-connections", true); this.acceptTransfers = config.getOrElse("accepts-transfers", false); + this.enableReusePort = config.getOrElse("enable-reuse-port", false); + this.commandRateLimit = config.getIntOrElse("command-rate-limit", 25); + this.forwardCommandsIfRateLimited = config.getOrElse("forward-commands-if-rate-limited", true); + this.kickAfterRateLimitedCommands = config.getIntOrElse("kick-after-rate-limited-commands", 0); + this.tabCompleteRateLimit = config.getIntOrElse("tab-complete-rate-limit", 10); // very lenient + this.kickAfterRateLimitedTabCompletes = config.getIntOrElse("kick-after-rate-limited-tab-completes", 0); } } @@ -828,6 +890,30 @@ public class VelocityConfiguration implements ProxyConfig { return this.acceptTransfers; } + public boolean isEnableReusePort() { + return enableReusePort; + } + + public int getCommandRateLimit() { + return commandRateLimit; + } + + public boolean isForwardCommandsIfRateLimited() { + return forwardCommandsIfRateLimited; + } + + public int getKickAfterRateLimitedCommands() { + return kickAfterRateLimitedCommands; + } + + public int getTabCompleteRateLimit() { + return tabCompleteRateLimit; + } + + public int getKickAfterRateLimitedTabCompletes() { + return kickAfterRateLimitedTabCompletes; + } + @Override public String toString() { return "Advanced{" @@ -845,6 +931,7 @@ public class VelocityConfiguration implements ProxyConfig { + ", logCommandExecutions=" + logCommandExecutions + ", logPlayerConnections=" + logPlayerConnections + ", acceptTransfers=" + acceptTransfers + + ", enableReusePort=" + enableReusePort + '}'; } } 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 c9173907..3d7ed448 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -46,6 +46,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueInboundHandler; import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler; @@ -84,6 +85,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private static final Logger logger = LogManager.getLogger(MinecraftConnection.class); private final Channel channel; + public boolean pendingConfigurationSwitch = false; private SocketAddress remoteAddress; private StateRegistry state; private Map sessionHandlers; @@ -367,6 +369,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { ensureInEventLoop(); this.state = state; + final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline() + .get(MinecraftVarintFrameDecoder.class); + if (frameDecoder != null) { + frameDecoder.setState(state); + } // If the connection is LEGACY (<1.6), the decoder and encoder are not set. final MinecraftEncoder minecraftEncoder = this.channel.pipeline() .get(MinecraftEncoder.class); 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 128d5c37..7c89f659 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 @@ -47,6 +47,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; @@ -149,6 +150,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { MinecraftConnection smc = serverConn.ensureConnected(); smc.setAutoReading(false); // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftVarintFrameDecoder.class).setState(StateRegistry.CONFIG); smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.CONFIG); serverConn.getPlayer().switchToConfigState(); return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java index 6161c4c2..a2ea94d6 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 @@ -20,6 +20,7 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -316,9 +317,9 @@ public class BungeeCordMessageResponder { }); } - static String getBungeeCordChannel(ProtocolVersion version) { - return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId() - : LEGACY_CHANNEL.getId(); + static ChannelIdentifier getBungeeCordChannel(ProtocolVersion version) { + return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL + : LEGACY_CHANNEL; } // Note: this method will always release the buffer! @@ -329,8 +330,8 @@ public class BungeeCordMessageResponder { // 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()); - PluginMessagePacket msg = new PluginMessagePacket(chan, buf); + ChannelIdentifier chan = getBungeeCordChannel(serverConnection.getProtocolVersion()); + PluginMessagePacket msg = new PluginMessagePacket(chan.getId(), buf); serverConnection.write(msg); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index 74f0576c..fb0e48d1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.connection.backend; +import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieStoreEvent; @@ -24,6 +25,7 @@ import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -38,6 +40,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; @@ -54,6 +57,8 @@ import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; import java.io.IOException; import java.net.InetSocketAddress; import java.util.concurrent.CompletableFuture; @@ -228,6 +233,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { final ConnectedPlayer player = serverConn.getPlayer(); final ClientConfigSessionHandler configHandler = (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); + smc.getChannel().pipeline().get(MinecraftVarintFrameDecoder.class).setState(StateRegistry.PLAY); smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); //noinspection DataFlowIssue configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> { @@ -261,7 +267,29 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), serverConn.getPlayer().getProtocolVersion())); } else { - serverConn.getPlayer().getConnection().write(packet.retain()); + byte[] bytes = ByteBufUtil.getBytes(packet.content()); + ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); + + if (id == null) { + serverConn.getPlayer().getConnection().write(packet.retain()); + return true; + } + + // Handling this stuff async means that we should probably pause + // the connection while we toss this off into another pool + this.serverConn.getConnection().setAutoReading(false); + this.server.getEventManager() + .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes)) + .thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed() && !serverConn.getPlayer().getConnection().isClosed()) { + serverConn.getPlayer().getConnection().write(new PluginMessagePacket( + pme.getIdentifier().getId(), Unpooled.wrappedBuffer(bytes))); + } + this.serverConn.getConnection().setAutoReading(true); + }, serverConn.ensureConnected().eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message {}", packet, ex); + return null; + }); } return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index 257429e2..0aea8d77 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -106,7 +106,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { return CompletableFuture.completedFuture(null); } - logger.info("{} has connected", player); + if (server.getConfiguration().isLogPlayerConnections()) { + logger.info("{} has connected", player); + } return server.getEventManager() .fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS)) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index 7bb7bedf..dc033f57 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -17,14 +17,17 @@ package com.velocitypowered.proxy.connection.client; +import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.configuration.PlayerConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -41,6 +44,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -123,8 +127,32 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { brandChannel = packet.getChannel(); // Client sends `minecraft:brand` packet immediately after Login, // but at this time the backend server may not be ready + } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { + return true; } else if (serverConn != null) { - serverConn.ensureConnected().write(packet.retain()); + byte[] bytes = ByteBufUtil.getBytes(packet.content()); + ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); + + if (id == null) { + serverConn.ensureConnected().write(packet.retain()); + return true; + } + + // Handling this stuff async means that we should probably pause + // the connection while we toss this off into another pool + serverConn.getPlayer().getConnection().setAutoReading(false); + this.server.getEventManager() + .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes)) + .thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed() && serverConn.getConnection() != null) { + serverConn.ensureConnected().write(new PluginMessagePacket( + pme.getIdentifier().getId(), Unpooled.wrappedBuffer(bytes))); + } + serverConn.getPlayer().getConnection().setAutoReading(true); + }, player.getConnection().eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message packet for {}", player, ex); + return null; + }); } return true; } @@ -142,7 +170,11 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KnownPacksPacket packet) { callConfigurationEvent().thenRun(() -> { - player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet); + VelocityServerConnection targetServer = + player.getConnectionInFlightOrConnectedServer(); + if (targetServer != null) { + targetServer.ensureConnected().write(packet); + } }).exceptionally(ex -> { logger.error("Error forwarding known packs response to backend:", ex); return null; 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 b41a24cc..10b837fa 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 @@ -30,8 +30,6 @@ import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; 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; @@ -74,6 +72,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; +import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; @@ -113,6 +112,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private CompletableFuture configSwitchFuture; + private int failedTabCompleteAttempts; + /** * Constructs a client play session handler. * @@ -160,7 +161,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { configSwitchFuture = new CompletableFuture<>(); - Collection channels = + Collection channels = server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels); @@ -170,6 +171,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void deactivated() { + player.discardChatQueue(); for (PluginMessagePacket message : loginPluginMessages) { ReferenceCountUtil.release(message); } @@ -307,20 +309,17 @@ 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)) { - List channels = PluginMessageUtil.getChannels(packet); - List channelIdentifiers = new ArrayList<>(); - for (String channel : channels) { - try { - channelIdentifiers.add(MinecraftChannelIdentifier.from(channel)); - } catch (IllegalArgumentException e) { - channelIdentifiers.add(new LegacyChannelIdentifier(channel)); - } - } + List channels = + PluginMessageUtil.getChannels(this.player.getClientsideChannels().size(), packet, + this.player.getProtocolVersion()); + player.getClientsideChannels().addAll(channels); server.getEventManager() .fireAndForget( - new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); + new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { + player.getClientsideChannels() + .removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion())); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -394,10 +393,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(FinishedUpdatePacket packet) { + if (!player.getConnection().pendingConfigurationSwitch) { + throw new QuietRuntimeException("Not expecting reconfiguration"); + } // Complete client switch player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); VelocityServerConnection serverConnection = player.getConnectedServer(); - server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); + server.getEventManager() + .fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); if (serverConnection != null) { MinecraftConnection smc = serverConnection.ensureConnected(); CompletableFuture.runAsync(() -> { @@ -444,6 +447,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(JoinGamePacket packet) { + // Forward the packet as normal, but discard any chat state we have queued - the client will do this too + player.discardChatQueue(); + return false; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); @@ -577,11 +587,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Tell the server about the proxy's plugin message channels. ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - final Collection channels = server.getChannelRegistrar() + final Collection channels = server.getChannelRegistrar() .getChannelsForProtocol(serverMc.getProtocolVersion()); if (!channels.isEmpty()) { serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); } + // Tell the server about this client's plugin message channels. + if (!player.getClientsideChannels().isEmpty()) { + serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getClientsideChannels())); + } // If we had plugin messages queued during login/FML handshake, send them now. PluginMessagePacket pm; @@ -663,6 +677,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return false; } + if (!server.getTabCompleteRateLimiter().attempt(player.getUniqueId())) { + if (server.getConfiguration().isKickOnTabCompleteRateLimit() + && failedTabCompleteAttempts++ >= server.getConfiguration().getKickAfterRateLimitedTabCompletes()) { + player.disconnect(Component.translatable("velocity.kick.tab-complete-rate-limit")); + } + + return true; + } + + failedTabCompleteAttempts = 0; + server.getCommandManager().offerBrigadierSuggestions(player, command) .thenAcceptAsync(suggestions -> { if (suggestions.isEmpty()) { 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 a7717055..1fb3884b 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 @@ -99,6 +99,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; import com.velocitypowered.proxy.util.TranslatableMapper; +import com.velocitypowered.proxy.util.collect.CappedSet; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; @@ -122,6 +123,7 @@ import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.platform.facet.FacetPointers; import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.pointer.Pointers; +import net.kyori.adventure.pointer.PointersSupplier; import net.kyori.adventure.resource.ResourcePackInfoLike; import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.resource.ResourcePackRequestLike; @@ -144,13 +146,23 @@ import org.jetbrains.annotations.NotNull; public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, VelocityInboundConnection { + public static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = Integer.getInteger("velocity.max-clientside-plugin-channels", 1024); private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; private static final ComponentLogger logger = ComponentLogger.logger(ConnectedPlayer.class); - private final Identity identity = new IdentityImpl(); + private static final @NotNull PointersSupplier POINTERS_SUPPLIER = + PointersSupplier.builder() + .resolving(Identity.UUID, Player::getUniqueId) + .resolving(Identity.NAME, Player::getUsername) + .resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername())) + .resolving(Identity.LOCALE, Player::getEffectiveLocale) + .resolving(PermissionChecker.POINTER, Player::getPermissionChecker) + .resolving(FacetPointers.TYPE, player -> Type.PLAYER) + .build(); + /** * The actual Minecraft connection. This is actually a wrapper object around the Netty channel. */ @@ -173,24 +185,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; + private final Collection clientsideChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; private final ResourcePackHandler resourcePackHandler; private final BundleDelimiterHandler bundleHandler = new BundleDelimiterHandler(this); - private final @NotNull Pointers pointers = - Player.super.pointers().toBuilder() - .withDynamic(Identity.UUID, this::getUniqueId) - .withDynamic(Identity.NAME, this::getUsername) - .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) - .withDynamic(Identity.LOCALE, this::getEffectiveLocale) - .withStatic(PermissionChecker.POINTER, getPermissionChecker()) - .withStatic(FacetPointers.TYPE, Type.PLAYER).build(); private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; private final @Nullable IdentifiedKey playerKey; private @Nullable ClientSettingsPacket clientSettingsPacket; - private final ChatQueue chatQueue; + private volatile ChatQueue chatQueue; private final ChatBuilderFactory chatBuilderFactory; ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, @@ -205,6 +210,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.connectionPhase = connection.getType().getInitialClientPhase(); this.onlineMode = onlineMode; + this.clientsideChannels = CappedSet.create(MAX_CLIENTSIDE_PLUGIN_CHANNELS); if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { this.tabList = new VelocityTabList(this); @@ -236,13 +242,24 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return chatQueue; } + /** + * Discards any messages still being processed by the {@link ChatQueue}, and creates a fresh state for future packets. + * This should be used on server switches, or whenever the client resets its own 'last seen' state. + */ + public void discardChatQueue() { + // No need for atomic swap, should only be called from event loop + final ChatQueue oldChatQueue = chatQueue; + chatQueue = new ChatQueue(this); + oldChatQueue.close(); + } + public BundleDelimiterHandler getBundleHandler() { return this.bundleHandler; } @Override public @NonNull Identity identity() { - return this.identity; + return Identity.identity(this.getUniqueId()); } @Override @@ -348,7 +365,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, @Override public @NotNull Pointers pointers() { - return this.pointers; + return POINTERS_SUPPLIER.view(this); } @Override @@ -381,14 +398,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } /** - * Translates the message in the user's locale. + * Translates the message in the user's locale, falling back to the default locale if not set. * * @param message the message to translate * @return the translated message */ public Component translateMessage(Component message) { - Locale locale = ClosestLocaleMatcher.INSTANCE - .lookupClosest(getEffectiveLocale() == null ? Locale.getDefault() : getEffectiveLocale()); + Locale locale = this.getEffectiveLocale(); + if (locale == null && settings != null) { + locale = settings.getLocale(); + } + if (locale == null) { + locale = Locale.getDefault(); + } + locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(locale); return GlobalTranslator.render(message, locale); } @@ -1090,8 +1113,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, throw new IllegalStateException("Can only send server links in CONFIGURATION or PLAY protocol"); } - connection.write(new ClientboundServerLinksPacket(List.copyOf(links).stream() - .map(l -> new ClientboundServerLinksPacket.ServerLink(l, getProtocolVersion())).toList())); + connection.write(new ClientboundServerLinksPacket(links.stream() + .map(l -> new ClientboundServerLinksPacket.ServerLink( + l.getBuiltInType().map(Enum::ordinal).orElse(-1), + l.getCustomLabel() + .map(c -> new ComponentHolder(getProtocolVersion(), translateMessage(c))) + .orElse(null), + l.getUrl().toString())) + .toList())); } @Override @@ -1287,11 +1316,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, public void switchToConfigState() { server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer())) .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> { + // if the connection was closed earlier, there is a risk that the player is no longer connected + if (!connection.getChannel().isActive()) { + return; + } + if (bundleHandler.isInBundleSession()) { bundleHandler.toggleBundleSession(); connection.write(BundleDelimiterPacket.INSTANCE); } connection.write(StartUpdatePacket.INSTANCE); + connection.pendingConfigurationSwitch = true; connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); // Make sure we don't send any play packets to the player after update start connection.addPlayPacketQueueHandler(); @@ -1320,19 +1355,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.connectionPhase = connectionPhase; } + /** + * Return all the plugin message channels that registered by client. + * + * @return the channels + */ + public Collection getClientsideChannels() { + return clientsideChannels; + } + @Override public @Nullable IdentifiedKey getIdentifiedKey() { return playerKey; } - private class IdentityImpl implements Identity { - - @Override - public @NonNull UUID uuid() { - return ConnectedPlayer.this.getUniqueId(); - } - } - @Override public ProtocolState getProtocolState() { return connection.getState().toProtocolState(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java index 1d066117..854a52d7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java @@ -64,6 +64,7 @@ class LegacyForgeUtil { if (discriminator == MOD_LIST_DISCRIMINATOR) { ImmutableList.Builder mods = ImmutableList.builder(); int modCount = ProtocolUtils.readVarInt(contents); + Preconditions.checkArgument(modCount < 1024, "Oversized mods list"); for (int index = 0; index < modCount; index++) { String id = ProtocolUtils.readString(contents); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java index 951d6098..d1f2b0db 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java @@ -30,10 +30,12 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; /** * Common utilities for handling server list ping results. @@ -51,11 +53,27 @@ public class ServerListPingHandler { version = ProtocolVersion.MAXIMUM_VERSION; } VelocityConfiguration configuration = server.getConfiguration(); + List samplePlayers; + if (configuration.getSamplePlayersInPing()) { + List unshuffledPlayers = server.getAllPlayers().stream() + .map(p -> { + if (p.getPlayerSettings().isClientListingAllowed()) { + return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId()); + } else { + return ServerPing.SamplePlayer.ANONYMOUS; + } + }) + .collect(Collectors.toList()); + Collections.shuffle(unshuffledPlayers); + samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount())); + } else { + samplePlayers = ImmutableList.of(); + } return new ServerPing( new ServerPing.Version(version.getProtocol(), "Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING), new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), - ImmutableList.of()), + samplePlayers), configuration.getMotd(), configuration.getFavicon().orElse(null), configuration.isAnnounceForge() ? ModInfo.DEFAULT : null @@ -63,7 +81,7 @@ public class ServerListPingHandler { } private CompletableFuture attemptPingPassthrough(VelocityInboundConnection connection, - PingPassthroughMode mode, List servers, ProtocolVersion responseProtocolVersion) { + PingPassthroughMode mode, List servers, ProtocolVersion responseProtocolVersion, String virtualHostStr) { ServerPing fallback = constructLocalPing(connection.getProtocolVersion()); List> pings = new ArrayList<>(); for (String s : servers) { @@ -73,7 +91,7 @@ public class ServerListPingHandler { } VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get(); pings.add(vrs.ping(connection.getConnection().eventLoop(), PingOptions.builder() - .version(responseProtocolVersion).build())); + .version(responseProtocolVersion).virtualHost(virtualHostStr).build())); } if (pings.isEmpty()) { return CompletableFuture.completedFuture(fallback); @@ -155,7 +173,7 @@ public class ServerListPingHandler { .orElse(""); List serversToTry = server.getConfiguration().getForcedHosts().getOrDefault( virtualHostStr, server.getConfiguration().getAttemptConnectionOrder()); - return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion); + return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion, virtualHostStr); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java index d60c6477..236394fb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java @@ -51,7 +51,7 @@ public class BackendChannelInitializer extends ChannelInitializer { @Override protected void initChannel(Channel ch) { ch.pipeline() - .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.CLIENTBOUND)) .addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.MILLISECONDS)) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 7af894f4..7b724f61 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -18,6 +18,8 @@ package com.velocitypowered.proxy.network; import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import com.velocitypowered.api.event.proxy.ListenerBoundEvent; import com.velocitypowered.api.event.proxy.ListenerCloseEvent; import com.velocitypowered.api.network.ListenerType; @@ -28,14 +30,17 @@ import com.velocitypowered.proxy.protocol.netty.GameSpyQueryHandler; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.unix.UnixChannelOption; import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.MultithreadEventExecutorGroup; import java.net.InetSocketAddress; import java.net.http.HttpClient; -import java.util.HashMap; +import java.util.Collection; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,7 +54,7 @@ public final class ConnectionManager { private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21); private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class); - private final Map endpoints = new HashMap<>(); + private final Multimap endpoints = HashMultimap.create(); private final TransportType transportType; private final EventLoopGroup bossGroup; private final EventLoopGroup workerGroup; @@ -93,7 +98,6 @@ public final class ConnectionManager { public void bind(final InetSocketAddress address) { final ServerBootstrap bootstrap = new ServerBootstrap() .channelFactory(this.transportType.serverSocketChannelFactory) - .group(this.bossGroup, this.workerGroup) .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK) .childHandler(this.serverChannelInitializer.get()) .childOption(ChannelOption.TCP_NODELAY, true) @@ -104,26 +108,50 @@ public final class ConnectionManager { bootstrap.option(ChannelOption.TCP_FASTOPEN, 3); } - bootstrap.bind() - .addListener((ChannelFutureListener) future -> { - final Channel channel = future.channel(); - if (future.isSuccess()) { - this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT)); - - // Warn people with console access that HAProxy is in use, see PR: #1436 - if (this.server.getConfiguration().isProxyProtocol()) { - LOGGER.warn("Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", channel.localAddress()); + if (server.getConfiguration().isEnableReusePort()) { + // We don't need a boss group, since each worker will bind to the socket + bootstrap.option(UnixChannelOption.SO_REUSEPORT, true) + .group(this.workerGroup); + } else { + bootstrap.group(this.bossGroup, this.workerGroup); + } + + final int binds = server.getConfiguration().isEnableReusePort() + ? ((MultithreadEventExecutorGroup) this.workerGroup).executorCount() : 1; + + for (int bind = 0; bind < binds; bind++) { + // Wait for each bind to open. If we encounter any errors, don't try to bind again. + int finalBind = bind; + ChannelFuture f = bootstrap.bind() + .addListener((ChannelFutureListener) future -> { + final Channel channel = future.channel(); + if (future.isSuccess()) { + this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT)); + + LOGGER.info("Listening on {}", channel.localAddress()); + + if (finalBind == 0) { + // Warn people with console access that HAProxy is in use, see PR: #1436 + if (this.server.getConfiguration().isProxyProtocol()) { + LOGGER.warn( + "Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", + channel.localAddress()); + } + + // Fire the proxy bound event after the socket is bound + server.getEventManager().fireAndForget( + new ListenerBoundEvent(address, ListenerType.MINECRAFT)); + } + } else { + LOGGER.error("Can't bind to {}", address, future.cause()); } + }); + f.syncUninterruptibly(); - LOGGER.info("Listening on {}", channel.localAddress()); - - // Fire the proxy bound event after the socket is bound - server.getEventManager().fireAndForget( - new ListenerBoundEvent(address, ListenerType.MINECRAFT)); - } else { - LOGGER.error("Can't bind to {}", address, future.cause()); - } - }); + if (!f.isSuccess()) { + break; + } + } } /** @@ -181,17 +209,20 @@ public final class ConnectionManager { * @param oldBind the endpoint to close */ public void close(InetSocketAddress oldBind) { - Endpoint endpoint = endpoints.remove(oldBind); + Collection endpoints = this.endpoints.removeAll(oldBind); + Preconditions.checkState(!endpoints.isEmpty(), "Endpoint was not registered"); + + ListenerType type = endpoints.iterator().next().getType(); // Fire proxy close event to notify plugins of socket close. We block since plugins // should have a chance to be notified before the server stops accepting connections. - server.getEventManager().fire(new ListenerCloseEvent(oldBind, endpoint.getType())).join(); + server.getEventManager().fire(new ListenerCloseEvent(oldBind, type)).join(); - Channel serverChannel = endpoint.getChannel(); - - Preconditions.checkState(serverChannel != null, "Endpoint %s not registered", oldBind); - LOGGER.info("Closing endpoint {}", serverChannel.localAddress()); - serverChannel.close().syncUninterruptibly(); + for (Endpoint endpoint : endpoints) { + Channel serverChannel = endpoint.getChannel(); + LOGGER.info("Closing endpoint {}", serverChannel.localAddress()); + serverChannel.close().syncUninterruptibly(); + } } /** @@ -200,24 +231,28 @@ public final class ConnectionManager { * @param interrupt should closing forward interruptions */ public void closeEndpoints(boolean interrupt) { - for (final Map.Entry entry : this.endpoints.entrySet()) { + for (final Map.Entry> entry : this.endpoints.asMap() + .entrySet()) { final InetSocketAddress address = entry.getKey(); - final Endpoint endpoint = entry.getValue(); + final Collection endpoints = entry.getValue(); + ListenerType type = endpoints.iterator().next().getType(); // Fire proxy close event to notify plugins of socket close. We block since plugins // should have a chance to be notified before the server stops accepting connections. - server.getEventManager().fire(new ListenerCloseEvent(address, endpoint.getType())).join(); + server.getEventManager().fire(new ListenerCloseEvent(address, type)).join(); - LOGGER.info("Closing endpoint {}", address); - if (interrupt) { - try { - endpoint.getChannel().close().sync(); - } catch (final InterruptedException e) { - LOGGER.info("Interrupted whilst closing endpoint", e); - Thread.currentThread().interrupt(); + for (Endpoint endpoint : endpoints) { + LOGGER.info("Closing endpoint {}", address); + if (interrupt) { + try { + endpoint.getChannel().close().sync(); + } catch (final InterruptedException e) { + LOGGER.info("Interrupted whilst closing endpoint", e); + Thread.currentThread().interrupt(); + } + } else { + endpoint.getChannel().close().syncUninterruptibly(); } - } else { - endpoint.getChannel().close().syncUninterruptibly(); } } this.endpoints.clear(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java index ef8e6b1c..0c22dcce 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java @@ -58,7 +58,7 @@ public class ServerChannelInitializer extends ChannelInitializer { protected void initChannel(final Channel ch) { ch.pipeline() .addLast(LEGACY_PING_DECODER, new LegacyPingDecoder()) - .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.SERVERBOUND)) .addLast(READ_TIMEOUT, new ReadTimeoutHandler(this.server.getConfiguration().getReadTimeout(), TimeUnit.MILLISECONDS)) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java index 6cbdfd8c..e154e5d5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -20,25 +20,32 @@ package com.velocitypowered.proxy.network; import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory; import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollDatagramChannel; -import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueueDatagramChannel; -import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueIoHandler; import io.netty.channel.kqueue.KQueueServerSocketChannel; import io.netty.channel.kqueue.KQueueSocketChannel; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringDatagramChannel; +import io.netty.channel.uring.IoUringIoHandler; +import io.netty.channel.uring.IoUringServerSocketChannel; +import io.netty.channel.uring.IoUringSocketChannel; import java.util.concurrent.ThreadFactory; -import java.util.function.BiFunction; +import java.util.function.Supplier; /** * Enumerates the supported transports for Velocity. @@ -47,32 +54,36 @@ public enum TransportType { NIO("NIO", NioServerSocketChannel::new, NioSocketChannel::new, NioDatagramChannel::new, - (name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))), + NioIoHandler::newFactory), EPOLL("epoll", EpollServerSocketChannel::new, EpollSocketChannel::new, EpollDatagramChannel::new, - (name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))), + EpollIoHandler::newFactory), KQUEUE("kqueue", KQueueServerSocketChannel::new, KQueueSocketChannel::new, KQueueDatagramChannel::new, - (name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type))); + KQueueIoHandler::newFactory), + IO_URING("io_uring", IoUringServerSocketChannel::new, + IoUringSocketChannel::new, + IoUringDatagramChannel::new, + IoUringIoHandler::newFactory); final String name; final ChannelFactory serverSocketChannelFactory; final ChannelFactory socketChannelFactory; final ChannelFactory datagramChannelFactory; - final BiFunction eventLoopGroupFactory; + final Supplier ioHandlerFactorySupplier; TransportType(final String name, final ChannelFactory serverSocketChannelFactory, final ChannelFactory socketChannelFactory, final ChannelFactory datagramChannelFactory, - final BiFunction eventLoopGroupFactory) { + final Supplier ioHandlerFactorySupplier) { this.name = name; this.serverSocketChannelFactory = serverSocketChannelFactory; this.socketChannelFactory = socketChannelFactory; this.datagramChannelFactory = datagramChannelFactory; - this.eventLoopGroupFactory = eventLoopGroupFactory; + this.ioHandlerFactorySupplier = ioHandlerFactorySupplier; } @Override @@ -80,8 +91,15 @@ public enum TransportType { return this.name; } + /** + * Creates a new event loop group for the given type. + * + * @param type the type of event loop group to create + * @return the event loop group + */ public EventLoopGroup createEventLoopGroup(final Type type) { - return this.eventLoopGroupFactory.apply(this.name, type); + return new MultiThreadIoEventLoopGroup( + 0, createThreadFactory(this.name, type), this.ioHandlerFactorySupplier.get()); } private static ThreadFactory createThreadFactory(final String name, final Type type) { @@ -98,6 +116,10 @@ public enum TransportType { return NIO; } + if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) { + return IO_URING; + } + if (Epoll.isAvailable()) { return EPOLL; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index ca63c1e5..a90ee9dc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -47,7 +47,7 @@ import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.json.JSONOptions; import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer; -import net.kyori.option.OptionState; +import net.kyori.option.OptionSchema; /** * Utilities for writing and reading data in the Minecraft protocol. @@ -60,14 +60,17 @@ public enum ProtocolUtils { .downsampleColors() .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .options( - OptionState.optionState() + OptionSchema.globalSchema().stateBuilder() // before 1.16 .value(JSONOptions.EMIT_RGB, Boolean.FALSE) - .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.LEGACY_ONLY) + .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.VALUE_FIELD) + .value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.CAMEL_CASE) // before 1.20.3 .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) + // before 1.21.5 + .value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE) .build() ) .build(); @@ -75,14 +78,37 @@ public enum ProtocolUtils { GsonComponentSerializer.builder() .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .options( - OptionState.optionState() + OptionSchema.globalSchema().stateBuilder() // after 1.16 .value(JSONOptions.EMIT_RGB, Boolean.TRUE) - .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.MODERN_ONLY) + .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE) + .value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.CAMEL_CASE) + .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, true) // before 1.20.3 .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) + // before 1.21.5 + .value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE) + .build() + ) + .build(); + private static final GsonComponentSerializer PRE_1_21_5_SERIALIZER = + GsonComponentSerializer.builder() + .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) + .options( + OptionSchema.globalSchema().stateBuilder() + // after 1.16 + .value(JSONOptions.EMIT_RGB, Boolean.TRUE) + .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE) + .value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.CAMEL_CASE) + .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, true) + // after 1.20.3 + .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE) + .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE) + .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE) + // before 1.21.5 + .value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE) .build() ) .build(); @@ -90,14 +116,18 @@ public enum ProtocolUtils { GsonComponentSerializer.builder() .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .options( - OptionState.optionState() + OptionSchema.globalSchema().stateBuilder() // after 1.16 .value(JSONOptions.EMIT_RGB, Boolean.TRUE) - .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.MODERN_ONLY) + .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.SNAKE_CASE) + .value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.SNAKE_CASE) // after 1.20.3 .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE) + // after 1.21.5 + .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE) + .value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.FALSE) .build() ) .build(); @@ -713,9 +743,12 @@ public enum ProtocolUtils { * @return the appropriate {@link GsonComponentSerializer} */ public static GsonComponentSerializer getJsonChatSerializer(ProtocolVersion version) { - if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) { + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_5)) { return MODERN_SERIALIZER; } + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) { + return PRE_1_21_5_SERIALIZER; + } if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) { return PRE_1_20_3_SERIALIZER; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 5a2fb8b8..f8c761ac 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -39,6 +39,8 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -255,7 +257,8 @@ public enum StateRegistry { map(0x09, MINECRAFT_1_19_4, false), map(0x0A, MINECRAFT_1_20_2, false), map(0x0B, MINECRAFT_1_20_5, false), - map(0x0D, MINECRAFT_1_21_2, false)); + map(0x0D, MINECRAFT_1_21_2, false), + map(0x0E, MINECRAFT_1_21_6, false)); serverbound.register( LegacyChatPacket.class, LegacyChatPacket::new, @@ -268,7 +271,8 @@ public enum StateRegistry { ChatAcknowledgementPacket.class, ChatAcknowledgementPacket::new, map(0x03, MINECRAFT_1_19_3, false), - map(0x04, MINECRAFT_1_21_2, false)); + map(0x04, MINECRAFT_1_21_2, false), + map(0x05, MINECRAFT_1_21_6, false)); serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new, map(0x03, MINECRAFT_1_19, false), map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); @@ -278,16 +282,19 @@ public enum StateRegistry { serverbound.register(SessionPlayerCommandPacket.class, SessionPlayerCommandPacket::new, map(0x04, MINECRAFT_1_19_3, false), map(0x05, MINECRAFT_1_20_5, false), - map(0x06, MINECRAFT_1_21_2, false)); + map(0x06, MINECRAFT_1_21_2, false), + map(0x07, MINECRAFT_1_21_6, false)); serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new, map(0x04, MINECRAFT_1_20_5, false), - map(0x05, MINECRAFT_1_21_2, false)); + map(0x05, MINECRAFT_1_21_2, false), + map(0x06, MINECRAFT_1_21_6, false)); serverbound.register( SessionPlayerChatPacket.class, SessionPlayerChatPacket::new, map(0x05, MINECRAFT_1_19_3, false), map(0x06, MINECRAFT_1_20_5, false), - map(0x07, MINECRAFT_1_21_2, false)); + map(0x07, MINECRAFT_1_21_2, false), + map(0x08, MINECRAFT_1_21_6, false)); serverbound.register( ClientSettingsPacket.class, ClientSettingsPacket::new, @@ -302,11 +309,13 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19_4, false), map(0x09, MINECRAFT_1_20_2, false), map(0x0A, MINECRAFT_1_20_5, false), - map(0x0C, MINECRAFT_1_21_2, false)); + map(0x0C, MINECRAFT_1_21_2, false), + map(0x0D, MINECRAFT_1_21_6, false)); serverbound.register( ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, map(0x11, MINECRAFT_1_20_5, false), - map(0x13, MINECRAFT_1_21_2, false)); + map(0x13, MINECRAFT_1_21_2, false), + map(0x14, MINECRAFT_1_21_6, false)); serverbound.register( PluginMessagePacket.class, PluginMessagePacket::new, @@ -324,7 +333,8 @@ public enum StateRegistry { map(0x0F, MINECRAFT_1_20_2, false), map(0x10, MINECRAFT_1_20_3, false), map(0x12, MINECRAFT_1_20_5, false), - map(0x14, MINECRAFT_1_21_2, false)); + map(0x14, MINECRAFT_1_21_2, false), + map(0x15, MINECRAFT_1_21_6, false)); serverbound.register( KeepAlivePacket.class, KeepAlivePacket::new, @@ -343,7 +353,8 @@ public enum StateRegistry { map(0x14, MINECRAFT_1_20_2, false), map(0x15, MINECRAFT_1_20_3, false), map(0x18, MINECRAFT_1_20_5, false), - map(0x1A, MINECRAFT_1_21_2, false)); + map(0x1A, MINECRAFT_1_21_2, false), + map(0x1B, MINECRAFT_1_21_6, false)); serverbound.register( ResourcePackResponsePacket.class, ResourcePackResponsePacket::new, @@ -360,12 +371,14 @@ public enum StateRegistry { map(0x28, MINECRAFT_1_20_3, false), map(0x2B, MINECRAFT_1_20_5, false), map(0x2D, MINECRAFT_1_21_2, false), - map(0x2F, MINECRAFT_1_21_4, false)); + map(0x2F, MINECRAFT_1_21_4, false), + map(0x30, MINECRAFT_1_21_6, false)); serverbound.register( FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, map(0x0B, MINECRAFT_1_20_2, false), map(0x0C, MINECRAFT_1_20_5, false), - map(0x0E, MINECRAFT_1_21_2, false)); + map(0x0E, MINECRAFT_1_21_2, false), + map(0x0F, MINECRAFT_1_21_6, false)); clientbound.register( BossBarPacket.class, @@ -376,7 +389,8 @@ public enum StateRegistry { map(0x0D, MINECRAFT_1_17, false), map(0x0A, MINECRAFT_1_19, false), map(0x0B, MINECRAFT_1_19_4, false), - map(0x0A, MINECRAFT_1_20_2, false)); + map(0x0A, MINECRAFT_1_20_2, false), + map(0x09, MINECRAFT_1_21_5, false)); clientbound.register( LegacyChatPacket.class, LegacyChatPacket::new, @@ -397,7 +411,8 @@ public enum StateRegistry { map(0x0E, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_3, false), map(0x0F, MINECRAFT_1_19_4, false), - map(0x10, MINECRAFT_1_20_2, false)); + map(0x10, MINECRAFT_1_20_2, false), + map(0x0F, MINECRAFT_1_21_5, false)); clientbound.register( AvailableCommandsPacket.class, AvailableCommandsPacket::new, @@ -409,10 +424,12 @@ public enum StateRegistry { map(0x0F, MINECRAFT_1_19, false), map(0x0E, MINECRAFT_1_19_3, false), map(0x10, MINECRAFT_1_19_4, false), - map(0x11, MINECRAFT_1_20_2, false)); + map(0x11, MINECRAFT_1_20_2, false), + map(0x10, MINECRAFT_1_21_5, false)); clientbound.register( ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, - map(0x16, MINECRAFT_1_20_5, false)); + map(0x16, MINECRAFT_1_20_5, false), + map(0x15, MINECRAFT_1_21_5, false)); clientbound.register( PluginMessagePacket.class, PluginMessagePacket::new, @@ -429,7 +446,8 @@ public enum StateRegistry { map(0x15, MINECRAFT_1_19_3, false), map(0x17, MINECRAFT_1_19_4, false), map(0x18, MINECRAFT_1_20_2, false), - map(0x19, MINECRAFT_1_20_5, false)); + map(0x19, MINECRAFT_1_20_5, false), + map(0x18, MINECRAFT_1_21_5, false)); clientbound.register( DisconnectPacket.class, () -> new DisconnectPacket(this), @@ -446,7 +464,8 @@ public enum StateRegistry { map(0x17, MINECRAFT_1_19_3, false), map(0x1A, MINECRAFT_1_19_4, false), map(0x1B, MINECRAFT_1_20_2, false), - map(0x1D, MINECRAFT_1_20_5, false)); + map(0x1D, MINECRAFT_1_20_5, false), + map(0x1C, MINECRAFT_1_21_5, false)); clientbound.register( KeepAlivePacket.class, KeepAlivePacket::new, @@ -464,7 +483,8 @@ public enum StateRegistry { map(0x23, MINECRAFT_1_19_4, false), map(0x24, MINECRAFT_1_20_2, false), map(0x26, MINECRAFT_1_20_5, false), - map(0x27, MINECRAFT_1_21_2, false)); + map(0x27, MINECRAFT_1_21_2, false), + map(0x26, MINECRAFT_1_21_5, false)); clientbound.register( JoinGamePacket.class, JoinGamePacket::new, @@ -482,7 +502,8 @@ public enum StateRegistry { map(0x28, MINECRAFT_1_19_4, false), map(0x29, MINECRAFT_1_20_2, false), map(0x2B, MINECRAFT_1_20_5, false), - map(0x2C, MINECRAFT_1_21_2, false)); + map(0x2C, MINECRAFT_1_21_2, false), + map(0x2B, MINECRAFT_1_21_5, false)); clientbound.register( RespawnPacket.class, RespawnPacket::new, @@ -503,13 +524,15 @@ public enum StateRegistry { map(0x43, MINECRAFT_1_20_2, true), map(0x45, MINECRAFT_1_20_3, true), map(0x47, MINECRAFT_1_20_5, true), - map(0x4C, MINECRAFT_1_21_2, true)); + map(0x4C, MINECRAFT_1_21_2, true), + map(0x4B, MINECRAFT_1_21_5, true)); clientbound.register( RemoveResourcePackPacket.class, RemoveResourcePackPacket::new, map(0x43, MINECRAFT_1_20_3, false), map(0x45, MINECRAFT_1_20_5, false), - map(0x4A, MINECRAFT_1_21_2, false)); + map(0x4A, MINECRAFT_1_21_2, false), + map(0x49, MINECRAFT_1_21_5, false)); clientbound.register( ResourcePackRequestPacket.class, ResourcePackRequestPacket::new, @@ -530,7 +553,8 @@ public enum StateRegistry { map(0x42, MINECRAFT_1_20_2, false), map(0x44, MINECRAFT_1_20_3, false), map(0x46, MINECRAFT_1_20_5, false), - map(0x4B, MINECRAFT_1_21_2, false)); + map(0x4B, MINECRAFT_1_21_2, false), + map(0x4A, MINECRAFT_1_21_5, false)); clientbound.register( HeaderAndFooterPacket.class, HeaderAndFooterPacket::new, @@ -552,7 +576,8 @@ public enum StateRegistry { map(0x68, MINECRAFT_1_20_2, true), map(0x6A, MINECRAFT_1_20_3, true), map(0x6D, MINECRAFT_1_20_5, true), - map(0x74, MINECRAFT_1_21_2, true)); + map(0x74, MINECRAFT_1_21_2, true), + map(0x73, MINECRAFT_1_21_5, true)); clientbound.register( LegacyTitlePacket.class, LegacyTitlePacket::new, @@ -573,7 +598,8 @@ public enum StateRegistry { map(0x5F, MINECRAFT_1_20_2, true), map(0x61, MINECRAFT_1_20_3, true), map(0x63, MINECRAFT_1_20_5, true), - map(0x6A, MINECRAFT_1_21_2, true)); + map(0x6A, MINECRAFT_1_21_2, true), + map(0x69, MINECRAFT_1_21_5, true)); clientbound.register( TitleTextPacket.class, TitleTextPacket::new, @@ -585,7 +611,8 @@ public enum StateRegistry { map(0x61, MINECRAFT_1_20_2, true), map(0x63, MINECRAFT_1_20_3, true), map(0x65, MINECRAFT_1_20_5, true), - map(0x6C, MINECRAFT_1_21_2, true)); + map(0x6C, MINECRAFT_1_21_2, true), + map(0x6B, MINECRAFT_1_21_5, true)); clientbound.register( TitleActionbarPacket.class, TitleActionbarPacket::new, @@ -597,7 +624,8 @@ public enum StateRegistry { map(0x48, MINECRAFT_1_20_2, true), map(0x4A, MINECRAFT_1_20_3, true), map(0x4C, MINECRAFT_1_20_5, true), - map(0x51, MINECRAFT_1_21_2, true)); + map(0x51, MINECRAFT_1_21_2, true), + map(0x50, MINECRAFT_1_21_5, true)); clientbound.register( TitleTimesPacket.class, TitleTimesPacket::new, @@ -609,7 +637,8 @@ public enum StateRegistry { map(0x62, MINECRAFT_1_20_2, true), map(0x64, MINECRAFT_1_20_3, true), map(0x66, MINECRAFT_1_20_5, true), - map(0x6D, MINECRAFT_1_21_2, true)); + map(0x6D, MINECRAFT_1_21_2, true), + map(0x6C, MINECRAFT_1_21_5, true)); clientbound.register( TitleClearPacket.class, TitleClearPacket::new, @@ -617,7 +646,8 @@ public enum StateRegistry { map(0x0D, MINECRAFT_1_19, true), map(0x0C, MINECRAFT_1_19_3, true), map(0x0E, MINECRAFT_1_19_4, true), - map(0x0F, MINECRAFT_1_20_2, true)); + map(0x0F, MINECRAFT_1_20_2, true), + map(0x0E, MINECRAFT_1_21_5, true)); clientbound.register( LegacyPlayerListItemPacket.class, LegacyPlayerListItemPacket::new, @@ -637,7 +667,8 @@ public enum StateRegistry { map(0x39, MINECRAFT_1_19_4, false), map(0x3B, MINECRAFT_1_20_2, false), map(0x3D, MINECRAFT_1_20_5, false), - map(0x3F, MINECRAFT_1_21_2, false)); + map(0x3F, MINECRAFT_1_21_2, false), + map(0x3E, MINECRAFT_1_21_5, false)); clientbound.register( UpsertPlayerInfoPacket.class, UpsertPlayerInfoPacket::new, @@ -645,11 +676,13 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_19_4, false), map(0x3C, MINECRAFT_1_20_2, false), map(0x3E, MINECRAFT_1_20_5, false), - map(0x40, MINECRAFT_1_21_2, false)); + map(0x40, MINECRAFT_1_21_2, false), + map(0x3F, MINECRAFT_1_21_5, false)); clientbound.register( ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, map(0x6B, MINECRAFT_1_20_5, false), - map(0x72, MINECRAFT_1_21_2, false)); + map(0x72, MINECRAFT_1_21_2, false), + map(0x71, MINECRAFT_1_21_5, false)); clientbound.register( SystemChatPacket.class, SystemChatPacket::new, @@ -660,7 +693,8 @@ public enum StateRegistry { map(0x67, MINECRAFT_1_20_2, true), map(0x69, MINECRAFT_1_20_3, true), map(0x6C, MINECRAFT_1_20_5, true), - map(0x73, MINECRAFT_1_21_2, true)); + map(0x73, MINECRAFT_1_21_2, true), + map(0x72, MINECRAFT_1_21_5, true)); clientbound.register( PlayerChatCompletionPacket.class, PlayerChatCompletionPacket::new, @@ -668,7 +702,8 @@ public enum StateRegistry { map(0x14, MINECRAFT_1_19_3, true), map(0x16, MINECRAFT_1_19_4, true), map(0x17, MINECRAFT_1_20_2, true), - map(0x18, MINECRAFT_1_20_5, true)); + map(0x18, MINECRAFT_1_20_5, true), + map(0x17, MINECRAFT_1_21_5, true)); clientbound.register( ServerDataPacket.class, ServerDataPacket::new, @@ -679,14 +714,16 @@ public enum StateRegistry { map(0x47, MINECRAFT_1_20_2, false), map(0x49, MINECRAFT_1_20_3, false), map(0x4B, MINECRAFT_1_20_5, false), - map(0x50, MINECRAFT_1_21_2, false)); + map(0x50, MINECRAFT_1_21_2, false), + map(0x4F, MINECRAFT_1_21_5, false)); clientbound.register( StartUpdatePacket.class, () -> StartUpdatePacket.INSTANCE, map(0x65, MINECRAFT_1_20_2, false), map(0x67, MINECRAFT_1_20_3, false), map(0x69, MINECRAFT_1_20_5, false), - map(0x70, MINECRAFT_1_21_2, false)); + map(0x70, MINECRAFT_1_21_2, false), + map(0x6F, MINECRAFT_1_21_5, false)); clientbound.register( BundleDelimiterPacket.class, () -> BundleDelimiterPacket.INSTANCE, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java index 6e7fb4d4..1fe38e50 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java @@ -39,6 +39,7 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder { private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("velocity.increased-compression-cap") ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; + private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation"); private int threshold; private final VelocityCompressor compressor; @@ -52,6 +53,11 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder { protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { int claimedUncompressedSize = ProtocolUtils.readVarInt(in); if (claimedUncompressedSize == 0) { + if (!SKIP_COMPRESSION_VALIDATION) { + int actualUncompressedSize = in.readableBytes(); + checkFrame(actualUncompressedSize < threshold, "Actual uncompressed size %s is greater than" + + " threshold %s", actualUncompressedSize, threshold); + } // This message is not compressed. out.add(in.retain()); return; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java index d75cb46c..5389de73 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java @@ -135,7 +135,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { private String getExtraConnectionDetail(int packetId) { return "Direction " + direction + " Protocol " + registry.version + " State " + state - + " ID " + Integer.toHexString(packetId); + + " ID 0x" + Integer.toHexString(packetId); } public void setProtocolVersion(ProtocolVersion protocolVersion) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java index 84f35381..5450390b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java @@ -19,21 +19,51 @@ package com.velocitypowered.proxy.protocol.netty; import static io.netty.util.ByteProcessor.FIND_NON_NUL; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.util.except.QuietDecoderException; +import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding. */ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { + private static final Logger LOGGER = LogManager.getLogger(MinecraftVarintFrameDecoder.class); + private static final QuietRuntimeException FRAME_DECODER_FAILED = + new QuietRuntimeException("A packet frame decoder failed. For more information, launch " + + "Velocity with -Dvelocity.packet-decode-logging=true to see more."); private static final QuietDecoderException BAD_PACKET_LENGTH = new QuietDecoderException("Bad packet length"); private static final QuietDecoderException VARINT_TOO_BIG = new QuietDecoderException("VarInt too big"); + private static final QuietDecoderException UNKNOWN_PACKET = + new QuietDecoderException("Unknown packet"); + + private final ProtocolUtils.Direction direction; + private final StateRegistry.PacketRegistry.ProtocolRegistry registry; + private StateRegistry state; + + /** + * Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}. + * + * @param direction the direction from which we decode from + */ + public MinecraftVarintFrameDecoder(ProtocolUtils.Direction direction) { + this.direction = direction; + this.registry = StateRegistry.HANDSHAKE.getProtocolRegistry( + direction, ProtocolVersion.MINIMUM_VERSION); + this.state = StateRegistry.HANDSHAKE; + } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) @@ -62,6 +92,43 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { throw BAD_PACKET_LENGTH; } + if (length > 0) { + if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) { + StateRegistry.PacketRegistry.ProtocolRegistry registry = + state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION); + + final int index = in.readerIndex(); + final int packetId = readRawVarInt21(in); + // Index hasn't changed, we've read nothing + if (index == in.readerIndex()) { + in.resetReaderIndex(); + return; + } + final int payloadLength = length - ProtocolUtils.varIntBytes(packetId); + + MinecraftPacket packet = registry.createPacket(packetId); + + // We handle every packet in this phase, if you said something we don't know, something is really wrong + if (packet == null) { + throw UNKNOWN_PACKET; + } + + // We 'technically' have the incoming bytes of a payload here, and so, these can actually parse + // the packet if needed, so, we'll take advantage of the existing methods + int expectedMinLen = packet.expectedMinLength(in, direction, registry.version); + int expectedMaxLen = packet.expectedMaxLength(in, direction, registry.version); + if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) { + throw handleOverflow(packet, expectedMaxLen, in.readableBytes()); + } + if (payloadLength < expectedMinLen) { + throw handleUnderflow(packet, expectedMaxLen, in.readableBytes()); + } + + + in.readerIndex(index); + } + } + // note that zero-length packets are ignored if (length > 0) { if (in.readableBytes() < length) { @@ -72,6 +139,16 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { } } + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (MinecraftDecoder.DEBUG) { + LOGGER.atWarn() + .withThrowable(cause) + .log("Exception caught while decoding frame for {}", ctx.channel().remoteAddress()); + } + super.exceptionCaught(ctx, cause); + } + /** * Reads a VarInt from the buffer of up to 21 bits in size. * @@ -141,4 +218,26 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { } return result | (tmp & 0x7F) << 14; } + + private Exception handleOverflow(MinecraftPacket packet, int expected, int actual) { + if (MinecraftDecoder.DEBUG) { + return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too " + + "big (expected " + expected + " bytes, got " + actual + " bytes)"); + } else { + return FRAME_DECODER_FAILED; + } + } + + private Exception handleUnderflow(MinecraftPacket packet, int expected, int actual) { + if (MinecraftDecoder.DEBUG) { + return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too " + + "small (expected " + expected + " bytes, got " + actual + " bytes)"); + } else { + return FRAME_DECODER_FAILED; + } + } + + public void setState(StateRegistry stateRegistry) { + this.state = stateRegistry; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java index 1687f902..08fe23b2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java @@ -50,12 +50,14 @@ import java.util.Deque; import java.util.Iterator; import java.util.Queue; import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; public class AvailableCommandsPacket implements MinecraftPacket { private static final Command PLACEHOLDER_COMMAND = source -> 0; + private static final Predicate PLACEHOLDER_REQUIREMENT = source -> true; private static final byte NODE_TYPE_ROOT = 0x00; private static final byte NODE_TYPE_LITERAL = 0x01; @@ -65,6 +67,7 @@ public class AvailableCommandsPacket implements MinecraftPacket { private static final byte FLAG_EXECUTABLE = 0x04; private static final byte FLAG_IS_REDIRECT = 0x08; private static final byte FLAG_HAS_SUGGESTIONS = 0x10; + private static final byte FLAG_IS_RESTRICTED = 0x20; private @MonotonicNonNull RootCommandNode rootNode; @@ -146,6 +149,9 @@ public class AvailableCommandsPacket implements MinecraftPacket { if (node.getCommand() != null) { flags |= FLAG_EXECUTABLE; } + if (node.getRequirement() == PLACEHOLDER_REQUIREMENT) { + flags |= FLAG_IS_RESTRICTED; + } if (node instanceof LiteralCommandNode) { flags |= NODE_TYPE_LITERAL; @@ -289,6 +295,11 @@ public class AvailableCommandsPacket implements MinecraftPacket { args.executes(PLACEHOLDER_COMMAND); } + // If restricted, add empty requirement + if ((flags & FLAG_IS_RESTRICTED) != 0) { + args.requires(PLACEHOLDER_REQUIREMENT); + } + this.built = args.build(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java index 2edaed9e..9eb7a93e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java @@ -106,4 +106,16 @@ public class HandshakePacket implements MinecraftPacket { public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); } + + @Override + public int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 7; + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3); + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java index 271d0c50..6441f6f7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java @@ -58,6 +58,13 @@ public class ArgumentIdentifier { this.versionById = ImmutableMap.copyOf(temp); } + @Override + public String toString() { + return "ArgumentIdentifier{" + + "identifier='" + identifier + '\'' + + '}'; + } + public String getIdentifier() { return identifier; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java index 7c04c97b..197fddf3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java @@ -22,6 +22,8 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet; import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; @@ -163,6 +165,7 @@ public class ArgumentPropertyRegistry { return i; } } + throw new IllegalArgumentException("Argument type identifier " + id + " unknown."); } else { String identifier = ProtocolUtils.readString(buf); for (ArgumentIdentifier i : byIdentifier.keySet()) { @@ -206,65 +209,79 @@ public class ArgumentPropertyRegistry { empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14))); empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15))); empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16))); - empty(id("minecraft:component", mapSet(MINECRAFT_1_19, 17))); - empty(id("minecraft:style", mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3 - empty(id("minecraft:message", mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18))); - empty(id("minecraft:nbt_compound_tag", mapSet(MINECRAFT_1_20_3, 20), mapSet(MINECRAFT_1_19, 19))); // added in 1.14 - empty(id("minecraft:nbt_tag", mapSet(MINECRAFT_1_20_3, 21), mapSet(MINECRAFT_1_19, 20))); // added in 1.14 - empty(id("minecraft:nbt_path", mapSet(MINECRAFT_1_20_3, 22), mapSet(MINECRAFT_1_19, 21))); - empty(id("minecraft:objective", mapSet(MINECRAFT_1_20_3, 23), mapSet(MINECRAFT_1_19, 22))); - empty(id("minecraft:objective_criteria", mapSet(MINECRAFT_1_20_3, 24), mapSet(MINECRAFT_1_19, 23))); - empty(id("minecraft:operation", mapSet(MINECRAFT_1_20_3, 25), mapSet(MINECRAFT_1_19, 24))); - empty(id("minecraft:particle", mapSet(MINECRAFT_1_20_3, 26), mapSet(MINECRAFT_1_19, 25))); - empty(id("minecraft:angle", mapSet(MINECRAFT_1_20_3, 27), mapSet(MINECRAFT_1_19, 26))); // added in 1.16.2 - empty(id("minecraft:rotation", mapSet(MINECRAFT_1_20_3, 28), mapSet(MINECRAFT_1_19, 27))); - empty(id("minecraft:scoreboard_slot", mapSet(MINECRAFT_1_20_3, 29), mapSet(MINECRAFT_1_19, 28))); - empty(id("minecraft:score_holder", mapSet(MINECRAFT_1_20_3, 30), mapSet(MINECRAFT_1_19, 29)), ByteArgumentPropertySerializer.BYTE); - empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_20_3, 31), mapSet(MINECRAFT_1_19, 30))); - empty(id("minecraft:team", mapSet(MINECRAFT_1_20_3, 32), mapSet(MINECRAFT_1_19, 31))); - empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_20_3, 33), mapSet(MINECRAFT_1_19, 32))); - empty(id("minecraft:item_slots", mapSet(MINECRAFT_1_20_5, 34))); // added 1.20.5 - empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_20_5, 35), mapSet(MINECRAFT_1_20_3, 34), mapSet(MINECRAFT_1_19, 33))); + empty(id("minecraft:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17))); + empty(id("minecraft:style", mapSet(MINECRAFT_1_21_6, 19), mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3 + empty(id("minecraft:message", mapSet(MINECRAFT_1_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18))); + empty(id("minecraft:nbt_compound_tag", mapSet(MINECRAFT_1_21_6, 21), mapSet(MINECRAFT_1_20_3, 20), mapSet(MINECRAFT_1_19, 19))); // added in 1.14 + empty(id("minecraft:nbt_tag", mapSet(MINECRAFT_1_21_6, 22), mapSet(MINECRAFT_1_20_3, 21), mapSet(MINECRAFT_1_19, 20))); // added in 1.14 + empty(id("minecraft:nbt_path", mapSet(MINECRAFT_1_21_6, 23), mapSet(MINECRAFT_1_20_3, 22), mapSet(MINECRAFT_1_19, 21))); + empty(id("minecraft:objective", mapSet(MINECRAFT_1_21_6, 24), mapSet(MINECRAFT_1_20_3, 23), mapSet(MINECRAFT_1_19, 22))); + empty(id("minecraft:objective_criteria", mapSet(MINECRAFT_1_21_6, 25), mapSet(MINECRAFT_1_20_3, 24), mapSet(MINECRAFT_1_19, 23))); + empty(id("minecraft:operation", mapSet(MINECRAFT_1_21_6, 26), mapSet(MINECRAFT_1_20_3, 25), mapSet(MINECRAFT_1_19, 24))); + empty(id("minecraft:particle", mapSet(MINECRAFT_1_21_6, 27), mapSet(MINECRAFT_1_20_3, 26), mapSet(MINECRAFT_1_19, 25))); + empty(id("minecraft:angle", mapSet(MINECRAFT_1_21_6, 28), mapSet(MINECRAFT_1_20_3, 27), mapSet(MINECRAFT_1_19, 26))); // added in 1.16.2 + empty(id("minecraft:rotation", mapSet(MINECRAFT_1_21_6, 29), mapSet(MINECRAFT_1_20_3, 28), mapSet(MINECRAFT_1_19, 27))); + empty(id("minecraft:scoreboard_slot", mapSet(MINECRAFT_1_21_6, 30), mapSet(MINECRAFT_1_20_3, 29), mapSet(MINECRAFT_1_19, 28))); + empty(id("minecraft:score_holder", mapSet(MINECRAFT_1_21_6, 31), mapSet(MINECRAFT_1_20_3, 30), mapSet(MINECRAFT_1_19, 29)), + ByteArgumentPropertySerializer.BYTE); + empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_21_6, 32), mapSet(MINECRAFT_1_20_3, 31), mapSet(MINECRAFT_1_19, 30))); + empty(id("minecraft:team", mapSet(MINECRAFT_1_21_6, 33), mapSet(MINECRAFT_1_20_3, 32), mapSet(MINECRAFT_1_19, 31))); + empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_21_6, 34), mapSet(MINECRAFT_1_20_3, 33), mapSet(MINECRAFT_1_19, 32))); + empty(id("minecraft:item_slots", mapSet(MINECRAFT_1_21_6, 35), mapSet(MINECRAFT_1_20_5, 34))); // added 1.20.5 + empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_21_6, 36), mapSet(MINECRAFT_1_20_5, 35), mapSet(MINECRAFT_1_20_3, 34), + mapSet(MINECRAFT_1_19, 33))); empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 34))); - empty(id("minecraft:function", mapSet(MINECRAFT_1_20_5, 36), mapSet(MINECRAFT_1_20_3, 35), mapSet(MINECRAFT_1_19_3, 34), - mapSet(MINECRAFT_1_19, 35))); - empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_20_5, 37), mapSet(MINECRAFT_1_20_3, 36), mapSet(MINECRAFT_1_19_3, 35), - mapSet(MINECRAFT_1_19, 36))); - empty(id("minecraft:int_range", mapSet(MINECRAFT_1_20_5, 38), mapSet(MINECRAFT_1_20_3, 37), mapSet(MINECRAFT_1_19_3, 36), - mapSet(MINECRAFT_1_19, 37))); - empty(id("minecraft:float_range", mapSet(MINECRAFT_1_20_5, 39), mapSet(MINECRAFT_1_20_3, 38), mapSet(MINECRAFT_1_19_3, 37), - mapSet(MINECRAFT_1_19, 38))); + empty(id("minecraft:function", mapSet(MINECRAFT_1_21_6, 37), mapSet(MINECRAFT_1_20_5, 36), mapSet(MINECRAFT_1_20_3, 35), + mapSet(MINECRAFT_1_19_3, 34), mapSet(MINECRAFT_1_19, 35))); + empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_21_6, 38), mapSet(MINECRAFT_1_20_5, 37), mapSet(MINECRAFT_1_20_3, 36), + mapSet(MINECRAFT_1_19_3, 35), mapSet(MINECRAFT_1_19, 36))); + empty(id("minecraft:int_range", mapSet(MINECRAFT_1_21_6, 39), mapSet(MINECRAFT_1_20_5, 38), mapSet(MINECRAFT_1_20_3, 37), + mapSet(MINECRAFT_1_19_3, 36), mapSet(MINECRAFT_1_19, 37))); + empty(id("minecraft:float_range", mapSet(MINECRAFT_1_21_6, 40), mapSet(MINECRAFT_1_20_5, 39), mapSet(MINECRAFT_1_20_3, 38), + mapSet(MINECRAFT_1_19_3, 37), mapSet(MINECRAFT_1_19, 38))); empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 39))); empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 40))); - empty(id("minecraft:dimension", mapSet(MINECRAFT_1_20_5, 40), mapSet(MINECRAFT_1_20_3, 39), mapSet(MINECRAFT_1_19_3, 38), - mapSet(MINECRAFT_1_19, 41))); - empty(id("minecraft:gamemode", mapSet(MINECRAFT_1_20_5, 41), mapSet(MINECRAFT_1_20_3, 40), mapSet(MINECRAFT_1_19_3, 39))); // 1.19.3 + empty(id("minecraft:dimension", mapSet(MINECRAFT_1_21_6, 41), mapSet(MINECRAFT_1_20_5, 40), mapSet(MINECRAFT_1_20_3, 39), + mapSet(MINECRAFT_1_19_3, 38), mapSet(MINECRAFT_1_19, 41))); + empty(id("minecraft:gamemode", mapSet(MINECRAFT_1_21_6, 42), mapSet(MINECRAFT_1_20_5, 41), mapSet(MINECRAFT_1_20_3, 40), + mapSet(MINECRAFT_1_19_3, 39))); // 1.19.3 - empty(id("minecraft:time", mapSet(MINECRAFT_1_20_5, 42), mapSet(MINECRAFT_1_20_3, 41), mapSet(MINECRAFT_1_19_3, 40), - mapSet(MINECRAFT_1_19, 42)), TimeArgumentSerializer.TIME); // added in 1.14 + empty(id("minecraft:time", mapSet(MINECRAFT_1_21_6, 43), mapSet(MINECRAFT_1_20_5, 42), mapSet(MINECRAFT_1_20_3, 41), + mapSet(MINECRAFT_1_19_3, 40), mapSet(MINECRAFT_1_19, 42)), TimeArgumentSerializer.TIME); // added in 1.14 - register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_20_5, 43), mapSet(MINECRAFT_1_20_3, 42), mapSet(MINECRAFT_1_19_3, 41), - mapSet(MINECRAFT_1_19, 43)), RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); - register(id("minecraft:resource_or_tag_key", mapSet(MINECRAFT_1_20_5, 44), mapSet(MINECRAFT_1_20_3, 43), mapSet(MINECRAFT_1_19_3, 42)), + register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_21_6, 44), mapSet(MINECRAFT_1_20_5, 43), mapSet(MINECRAFT_1_20_3, 42), + mapSet(MINECRAFT_1_19_3, 41), mapSet(MINECRAFT_1_19, 43)), RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + register(id("minecraft:resource_or_tag_key", mapSet(MINECRAFT_1_21_6, 45), mapSet(MINECRAFT_1_20_5, 44), mapSet(MINECRAFT_1_20_3, 43), + mapSet(MINECRAFT_1_19_3, 42)), RegistryKeyArgumentList.ResourceOrTagKey.class, RegistryKeyArgumentList.ResourceOrTagKey.Serializer.REGISTRY); - register(id("minecraft:resource", mapSet(MINECRAFT_1_20_5, 45), mapSet(MINECRAFT_1_20_3, 44), mapSet(MINECRAFT_1_19_3, 43), - mapSet(MINECRAFT_1_19, 44)), + register(id("minecraft:resource", mapSet(MINECRAFT_1_21_6, 46), mapSet(MINECRAFT_1_20_5, 45), mapSet(MINECRAFT_1_20_3, 44), + mapSet(MINECRAFT_1_19_3, 43), mapSet(MINECRAFT_1_19, 44)), RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); - register(id("minecraft:resource_key", mapSet(MINECRAFT_1_20_5, 46), mapSet(MINECRAFT_1_20_3, 45), mapSet(MINECRAFT_1_19_3, 44)), + register(id("minecraft:resource_key", mapSet(MINECRAFT_1_21_6, 47), mapSet(MINECRAFT_1_20_5, 46), mapSet(MINECRAFT_1_20_3, 45), + mapSet(MINECRAFT_1_19_3, 44)), RegistryKeyArgumentList.ResourceKey.class, RegistryKeyArgumentList.ResourceKey.Serializer.REGISTRY); + register(id("minecraft:resource_selector", mapSet(MINECRAFT_1_21_6, 48), mapSet(MINECRAFT_1_21_5, 47)), + RegistryKeyArgumentList.ResourceSelector.class, + RegistryKeyArgumentList.ResourceSelector.Serializer.REGISTRY); - empty(id("minecraft:template_mirror", mapSet(MINECRAFT_1_20_5, 47), mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19 - empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_20_5, 48), mapSet(MINECRAFT_1_20_3, 47), mapSet(MINECRAFT_1_19, 46))); // 1.19 - empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_20_3, 49), mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4 + empty(id("minecraft:template_mirror", mapSet(MINECRAFT_1_21_6, 49), mapSet(MINECRAFT_1_21_5, 48), mapSet(MINECRAFT_1_20_5, 47), + mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19 + empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_21_6, 50), mapSet(MINECRAFT_1_21_5, 49), mapSet(MINECRAFT_1_20_5, 48), + mapSet(MINECRAFT_1_20_3, 47), mapSet(MINECRAFT_1_19, 46))); // 1.19 + empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_21_6, 51), mapSet(MINECRAFT_1_21_5, 50), mapSet(MINECRAFT_1_20_3, 49), + mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4 - empty(id("minecraft:uuid", mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48), mapSet(MINECRAFT_1_19_4, 48), - mapSet(MINECRAFT_1_19, 47))); // added in 1.16 + empty(id("minecraft:uuid", mapSet(MINECRAFT_1_21_6, 56), mapSet(MINECRAFT_1_21_5, 54),mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48), + mapSet(MINECRAFT_1_19_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16 - empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_20_5, 50))); - empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_20_5, 51))); - empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_20_5, 52))); + empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_21_6, 52), mapSet(MINECRAFT_1_21_5, 51), mapSet(MINECRAFT_1_20_5, 50))); + empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_21_6, 53), mapSet(MINECRAFT_1_21_5, 52), mapSet(MINECRAFT_1_20_5, 51))); + empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_21_6, 54), mapSet(MINECRAFT_1_21_5, 53), mapSet(MINECRAFT_1_20_5, 52))); + + empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6 + empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6 // Crossstitch support register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java index fbd0b623..2bf5f345 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentList.java @@ -67,23 +67,23 @@ public final class RegistryKeyArgumentList { } } - public static class Resource extends RegistryKeyArgument { + public static class ResourceSelector extends RegistryKeyArgument { - public Resource(String identifier) { + public ResourceSelector(String identifier) { super(identifier); } - public static class Serializer implements ArgumentPropertySerializer { + public static class Serializer implements ArgumentPropertySerializer { - static final Resource.Serializer REGISTRY = new Resource.Serializer(); + static final ResourceSelector.Serializer REGISTRY = new ResourceSelector.Serializer(); @Override - public Resource deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { - return new Resource(ProtocolUtils.readString(buf)); + public ResourceSelector deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { + return new ResourceSelector(ProtocolUtils.readString(buf)); } @Override - public void serialize(Resource object, ByteBuf buf, ProtocolVersion protocolVersion) { + public void serialize(ResourceSelector object, ByteBuf buf, ProtocolVersion protocolVersion) { ProtocolUtils.writeString(buf, object.getIdentifier()); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java index 5928bf36..14a56336 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java @@ -32,13 +32,15 @@ import java.util.function.Function; * A precisely ordered queue which allows for outside entries into the ordered queue through * piggybacking timestamps. */ -public class ChatQueue { +public class ChatQueue implements AutoCloseable { private final Object internalLock = new Object(); private final ConnectedPlayer player; private final ChatState chatState = new ChatState(); private CompletableFuture head = CompletableFuture.completedFuture(null); + private volatile boolean closed; + /** * Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}. * @@ -50,8 +52,14 @@ public class ChatQueue { private void queueTask(Task task) { synchronized (internalLock) { + if (closed) { + throw new IllegalStateException("ChatQueue has already been closed"); + } MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); head = head.thenCompose(v -> { + if (closed) { + return CompletableFuture.completedFuture(null); + } try { return task.update(chatState, smc).exceptionally(ignored -> null); } catch (Throwable ignored) { @@ -102,9 +110,9 @@ public class ChatQueue { }); } - private static CompletableFuture writePacket(T packet, MinecraftConnection smc) { + private CompletableFuture writePacket(T packet, MinecraftConnection smc) { return CompletableFuture.runAsync(() -> { - if (!smc.isClosed()) { + if (!closed && !smc.isClosed()) { ChannelFuture future = smc.write(packet); if (future != null) { future.awaitUninterruptibly(); @@ -113,6 +121,11 @@ public class ChatQueue { }, smc.eventLoop()); } + @Override + public void close() { + closed = true; + } + private interface Task { CompletableFuture update(ChatState chatState, MinecraftConnection smc); } @@ -174,7 +187,7 @@ public class ChatQueue { } public LastSeenMessages createLastSeen() { - return new LastSeenMessages(0, lastSeenMessages); + return new LastSeenMessages(0, lastSeenMessages, (byte) 0); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java index 0935f8ea..22b49621 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java @@ -43,7 +43,6 @@ import net.kyori.adventure.nbt.LongBinaryTag; import net.kyori.adventure.nbt.ShortBinaryTag; import net.kyori.adventure.nbt.StringBinaryTag; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -106,16 +105,14 @@ public class ComponentHolder { public BinaryTag getBinaryTag() { if (binaryTag == null) { // TODO: replace this with adventure-text-serializer-nbt - binaryTag = serialize(GsonComponentSerializer.gson().serializeToTree(getComponent())); + binaryTag = serialize(ProtocolUtils.getJsonChatSerializer(version).serializeToTree(getComponent())); } return binaryTag; } public static BinaryTag serialize(JsonElement json) { - if (json instanceof JsonPrimitive) { - JsonPrimitive jsonPrimitive = (JsonPrimitive) json; - - if (jsonPrimitive.isNumber()) { + if (json instanceof JsonPrimitive jsonPrimitive) { + if (jsonPrimitive.isNumber()) { Number number = json.getAsNumber(); if (number instanceof Byte) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java index 18a743c8..c03e5f8c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.protocol.packet.chat; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.Arrays; @@ -26,30 +27,38 @@ public class LastSeenMessages { public static final int WINDOW_SIZE = 20; private static final int DIV_FLOOR = -Math.floorDiv(-WINDOW_SIZE, 8); - private int offset; - private BitSet acknowledged; + private final int offset; + private final BitSet acknowledged; + private byte checksum; public LastSeenMessages() { - this.offset = 0; - this.acknowledged = new BitSet(); + this(0, new BitSet(), (byte) 0); } - public LastSeenMessages(int offset, BitSet acknowledged) { + public LastSeenMessages(int offset, BitSet acknowledged, byte checksum) { this.offset = offset; this.acknowledged = acknowledged; + this.checksum = checksum; } - public LastSeenMessages(ByteBuf buf) { + public LastSeenMessages(ByteBuf buf, ProtocolVersion protocolVersion) { this.offset = ProtocolUtils.readVarInt(buf); byte[] bytes = new byte[DIV_FLOOR]; buf.readBytes(bytes); this.acknowledged = BitSet.valueOf(bytes); + + if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_21_5)) { + this.checksum = buf.readByte(); + } } - public void encode(ByteBuf buf) { + public void encode(ByteBuf buf, ProtocolVersion protocolVersion) { ProtocolUtils.writeVarInt(buf, offset); buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR)); + if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_21_5)) { + buf.writeByte(this.checksum); + } } public int getOffset() { @@ -61,14 +70,15 @@ public class LastSeenMessages { } public LastSeenMessages offset(final int offset) { - return new LastSeenMessages(this.offset + offset, acknowledged); + return new LastSeenMessages(this.offset + offset, acknowledged, checksum); } @Override public String toString() { return "LastSeenMessages{" + - "offset=" + offset + - ", acknowledged=" + acknowledged + - '}'; + "offset=" + offset + + ", acknowledged=" + acknowledged + + ", checksum=" + checksum + + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java new file mode 100644 index 00000000..e5823643 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import net.kyori.adventure.text.Component; + +public abstract class RateLimitedCommandHandler implements CommandHandler { + + private final Player player; + private final VelocityServer velocityServer; + + private int failedAttempts; + + protected RateLimitedCommandHandler(Player player, VelocityServer velocityServer) { + this.player = player; + this.velocityServer = velocityServer; + } + + @Override + public boolean handlePlayerCommand(MinecraftPacket packet) { + if (packetClass().isInstance(packet)) { + if (!velocityServer.getCommandRateLimiter().attempt(player.getUniqueId())) { + if (velocityServer.getConfiguration().isKickOnCommandRateLimit() && failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) { + player.disconnect(Component.translatable("velocity.kick.command-rate-limit")); + } + + if (velocityServer.getConfiguration().isForwardCommandsIfRateLimited()) { + return false; // Send the packet to the server + } + } else { + failedAttempts = 0; + } + + handlePlayerCommandInternal(packetClass().cast(packet)); + return true; + } + + return false; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java index 5baedfb4..b10332f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -21,17 +21,18 @@ import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; import java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; -public class KeyedCommandHandler implements CommandHandler { +public class KeyedCommandHandler extends RateLimitedCommandHandler { private final ConnectedPlayer player; private final VelocityServer server; public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) { + super(player, server); this.player = player; this.server = server; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatPacket.java index ccefdd13..80b239d5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyChatPacket.java @@ -91,7 +91,8 @@ public class LegacyChatPacket implements MinecraftPacket { @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - message = ProtocolUtils.readString(buf, 256); + message = ProtocolUtils.readString(buf, direction == ProtocolUtils.Direction.CLIENTBOUND + ? 262144 : version.noLessThan(ProtocolVersion.MINECRAFT_1_11) ? 256 : 100); if (direction == ProtocolUtils.Direction.CLIENTBOUND && version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) { type = buf.readByte(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java index 30ad2c99..4f88ee34 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java @@ -20,16 +20,18 @@ package com.velocitypowered.proxy.protocol.packet.chat.legacy; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; +import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler; + import java.time.Instant; import java.util.concurrent.CompletableFuture; -public class LegacyCommandHandler implements CommandHandler { +public class LegacyCommandHandler extends RateLimitedCommandHandler { private final ConnectedPlayer player; private final VelocityServer server; public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) { + super(player, server); this.player = player; this.server = server; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 8d1dc0f2..6978f8ee 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -22,17 +22,19 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; -import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import java.util.concurrent.CompletableFuture; + +import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; -public class SessionCommandHandler implements CommandHandler { +public class SessionCommandHandler extends RateLimitedCommandHandler { private final ConnectedPlayer player; private final VelocityServer server; public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) { + super(player, server); this.player = player; this.server = server; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java index 41f37366..8a00452c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java @@ -73,7 +73,7 @@ public class SessionPlayerChatPacket implements MinecraftPacket { } else { this.signature = new byte[0]; } - this.lastSeenMessages = new LastSeenMessages(buf); + this.lastSeenMessages = new LastSeenMessages(buf, protocolVersion); } @Override @@ -86,7 +86,7 @@ public class SessionPlayerChatPacket implements MinecraftPacket { if (this.signed) { buf.writeBytes(this.signature); } - this.lastSeenMessages.encode(buf); + this.lastSeenMessages.encode(buf, protocolVersion); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java index 9084f4b5..993587dc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java @@ -45,7 +45,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { this.timeStamp = Instant.ofEpochMilli(buf.readLong()); this.salt = buf.readLong(); this.argumentSignatures = new ArgumentSignatures(buf); - this.lastSeenMessages = new LastSeenMessages(buf); + this.lastSeenMessages = new LastSeenMessages(buf, protocolVersion); } @Override @@ -54,7 +54,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { buf.writeLong(this.timeStamp.toEpochMilli()); buf.writeLong(this.salt); this.argumentSignatures.encode(buf); - this.lastSeenMessages.encode(buf); + this.lastSeenMessages.encode(buf, protocolVersion); } public String getCommand() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java index 274bbb8f..d37866d8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java @@ -68,12 +68,6 @@ public class ClientboundServerLinksPacket implements MinecraftPacket { public record ServerLink(int id, ComponentHolder displayName, String url) { - public ServerLink(com.velocitypowered.api.util.ServerLink link, ProtocolVersion protocolVersion) { - this(link.getBuiltInType().map(Enum::ordinal).orElse(-1), - link.getCustomLabel().map(c -> new ComponentHolder(protocolVersion, c)).orElse(null), - link.getUrl().toString()); - } - private static ServerLink read(ByteBuf buf, ProtocolVersion version) { if (buf.readBoolean()) { return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/KnownPacksPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/KnownPacksPacket.java index 3a7d60cb..b3fb0de4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/KnownPacksPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/KnownPacksPacket.java @@ -36,7 +36,7 @@ public class KnownPacksPacket implements MinecraftPacket { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { final int packCount = ProtocolUtils.readVarInt(buf); - if (packCount > MAX_LENGTH_PACKS) { + if (direction == ProtocolUtils.Direction.SERVERBOUND && packCount > MAX_LENGTH_PACKS) { throw TOO_MANY_PACKS; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 1cfd4a6f..1936eb3f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -22,13 +22,20 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; 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.api.util.ProxyVersion; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; +import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @@ -85,13 +92,18 @@ public final class PluginMessageUtil { .equals(UNREGISTER_CHANNEL); } + private static final QuietDecoderException ILLEGAL_CHANNEL = new QuietDecoderException("Illegal channel"); + /** * Fetches all the channels in a register or unregister plugin message. * + * @param existingChannels the number of channels already registered * @param message the message to get the channels from * @return the channels, as an immutable list */ - public static List getChannels(PluginMessagePacket message) { + public static List getChannels(int existingChannels, + PluginMessagePacket message, + ProtocolVersion protocolVersion) { checkNotNull(message, "message"); checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s", message.getChannel()); @@ -100,8 +112,28 @@ public final class PluginMessageUtil { // has caused issues with 1.13+ compatibility. Just return an empty list. return ImmutableList.of(); } - String channels = message.content().toString(StandardCharsets.UTF_8); - return ImmutableList.copyOf(channels.split("\0")); + String payload = message.content().toString(StandardCharsets.UTF_8); + checkArgument(payload.length() <= Short.MAX_VALUE, "payload too long: %s", payload.length()); + String[] channels = payload.split("\0"); + checkArgument(existingChannels + channels.length <= ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS, + "too many channels: %s + %s > %s", existingChannels, channels.length, ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS); + ImmutableList.Builder channelIdentifiers = ImmutableList.builderWithExpectedSize(channels.length); + try { + for (String channel : channels) { + if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) { + channelIdentifiers.add(MinecraftChannelIdentifier.from(channel)); + } else { + channelIdentifiers.add(new LegacyChannelIdentifier(channel)); + } + } + } catch (IllegalArgumentException e) { + if (MinecraftDecoder.DEBUG) { + throw e; + } else { + throw ILLEGAL_CHANNEL; + } + } + return channelIdentifiers.build(); } /** @@ -112,16 +144,31 @@ public final class PluginMessageUtil { * @return the plugin message to send */ public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion, - Collection channels) { + Collection channels) { checkNotNull(channels, "channels"); checkArgument(!channels.isEmpty(), "no channels specified"); String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY; ByteBuf contents = Unpooled.buffer(); - contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8); + contents.writeCharSequence(joinChannels(channels), StandardCharsets.UTF_8); return new PluginMessagePacket(channelName, contents); } + private static String joinChannels(Collection channels) { + checkNotNull(channels, "channels"); + checkArgument(!channels.isEmpty(), "no channels specified"); + StringBuilder sb = new StringBuilder(); + Iterator iterator = channels.iterator(); + while (iterator.hasNext()) { + ChannelIdentifier channel = iterator.next(); + sb.append(channel.getId()); + if (iterator.hasNext()) { + sb.append('\0'); + } + } + return sb.toString(); + } + /** * Rewrites the brand message to indicate the presence of Velocity. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 89760344..e58c72b4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -43,20 +43,23 @@ public class PingSessionHandler implements MinecraftSessionHandler { private final MinecraftConnection connection; private final ProtocolVersion version; private boolean completed = false; + private final String virtualHostString; PingSessionHandler(CompletableFuture result, RegisteredServer server, - MinecraftConnection connection, ProtocolVersion version) { + MinecraftConnection connection, ProtocolVersion version, String virtualHostString) { this.result = result; this.server = server; this.connection = connection; this.version = version; + this.virtualHostString = virtualHostString; } @Override public void activated() { HandshakePacket handshake = new HandshakePacket(); handshake.setIntent(HandshakeIntent.STATUS); - handshake.setServerAddress(server.getServerInfo().getAddress().getHostString()); + handshake.setServerAddress(this.virtualHostString == null || this.virtualHostString.isEmpty() + ? server.getServerInfo().getAddress().getHostString() : this.virtualHostString); handshake.setPort(server.getServerInfo().getAddress().getPort()); handshake.setProtocolVersion(version); connection.delayedWrite(handshake); 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 a3ddd71c..e48881f3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -114,7 +114,7 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud server.createBootstrap(loop).handler(new ChannelInitializer<>() { @Override protected void initChannel(Channel ch) { - ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.CLIENTBOUND)) .addLast(READ_TIMEOUT, new ReadTimeoutHandler( pingOptions.getTimeout() == 0 ? server.getConfiguration().getReadTimeout() @@ -129,7 +129,7 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud if (future.isSuccess()) { MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); PingSessionHandler handler = new PingSessionHandler(pingFuture, - VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion()); + VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion(), pingOptions.getVirtualHost()); conn.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler); } else { pingFuture.completeExceptionally(future.cause()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java index daaa9b0a..644a7603 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java @@ -166,7 +166,7 @@ public class KeyedVelocityTabList implements InternalTabList { @Override public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, - int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder) { + int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) { return buildEntry(profile, displayName, latency, gameMode, chatSession, listed); } 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 0990119c..249bd3b7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -90,7 +90,7 @@ public class VelocityTabList implements InternalTabList { } else { entry = new VelocityTabListEntry(this, entry1.getProfile(), entry1.getDisplayNameComponent().orElse(null), - entry1.getLatency(), entry1.getGameMode(), entry1.getChatSession(), entry1.isListed(), entry1.getListOrder()); + entry1.getLatency(), entry1.getGameMode(), entry1.getChatSession(), entry1.isListed(), entry1.getListOrder(), entry1.isShowHat()); } EnumSet actions = EnumSet @@ -134,6 +134,11 @@ public class VelocityTabList implements InternalTabList { actions.add(UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER); playerInfoEntry.setListOrder(entry.getListOrder()); } + if (!Objects.equals(previousEntry.isShowHat(), entry.isShowHat()) + && player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_4)) { + actions.add(UpsertPlayerInfoPacket.Action.UPDATE_HAT); + playerInfoEntry.setShowHat(entry.isShowHat()); + } if (!Objects.equals(previousEntry.getChatSession(), entry.getChatSession())) { ChatSession from = entry.getChatSession(); if (from != null) { @@ -173,6 +178,10 @@ public class VelocityTabList implements InternalTabList { actions.add(UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER); playerInfoEntry.setListOrder(entry.getListOrder()); } + if (!entry.isShowHat() && player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_4)) { + actions.add(UpsertPlayerInfoPacket.Action.UPDATE_HAT); + playerInfoEntry.setShowHat(entry.isShowHat()); + } } return entry; }); @@ -218,9 +227,9 @@ public class VelocityTabList implements InternalTabList { @Override public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode, - @Nullable ChatSession chatSession, boolean listed, int listOrder) { + @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) { return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, chatSession, - listed, listOrder); + listed, listOrder, showHat); } @Override @@ -258,7 +267,8 @@ public class VelocityTabList implements InternalTabList { -1, null, false, - 0 + 0, + true ) ); } else { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java index 352d6271..96592dc2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java @@ -40,6 +40,7 @@ public class VelocityTabListEntry implements TabListEntry { private int gameMode; private boolean listed; private int listOrder; + private boolean showHat; private @Nullable ChatSession session; /** @@ -47,7 +48,7 @@ public class VelocityTabListEntry implements TabListEntry { */ public VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, Component displayName, int latency, - int gameMode, @Nullable ChatSession session, boolean listed, int listOrder) { + int gameMode, @Nullable ChatSession session, boolean listed, int listOrder, boolean showHat) { this.tabList = tabList; this.profile = profile; this.displayName = displayName; @@ -56,6 +57,7 @@ public class VelocityTabListEntry implements TabListEntry { this.session = session; this.listed = listed; this.listOrder = listOrder; + this.showHat = showHat; } @Override @@ -173,4 +175,24 @@ public class VelocityTabListEntry implements TabListEntry { void setListOrderWithoutUpdate(int listOrder) { this.listOrder = listOrder; } + + @Override + public boolean isShowHat() { + return showHat; + } + + @Override + public VelocityTabListEntry setShowHat(boolean showHat) { + this.showHat = showHat; + if (tabList.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_4)) { + UpsertPlayerInfoPacket.Entry upsertEntry = this.tabList.createRawEntry(this); + upsertEntry.setShowHat(showHat); + tabList.emitActionRaw(UpsertPlayerInfoPacket.Action.UPDATE_HAT, upsertEntry); + } + return this; + } + + void setShowHatWithoutUpdate(boolean showHat) { + this.showHat = showHat; + } } \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java index cd5e58db..56418d9d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java @@ -35,6 +35,8 @@ public class VelocityTabListEntryLegacy extends KeyedVelocityTabListEntry { @Override public TabListEntry setDisplayName(@Nullable Component displayName) { getTabList().removeEntry(getProfile().getId()); // We have to remove first if updating - return super.setDisplayName(displayName); + setDisplayNameInternal(displayName); + getTabList().addEntry(this); + return this; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java index 1eeb180e..a49d24de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java @@ -19,6 +19,8 @@ package com.velocitypowered.proxy.tablist; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.player.ChatSession; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; @@ -133,9 +135,22 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList { } } + @Override + public TabListEntry buildEntry(GameProfile profile, + net.kyori.adventure.text.@Nullable Component displayName, + int latency, int gameMode, @Nullable IdentifiedKey key) { + return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode); + } + @Override public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, - int gameMode) { + int gameMode, @Nullable ChatSession chatSession, boolean listed) { + return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode); + } + + @Override + public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, + int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) { return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java index edc06761..6f30d3d1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java @@ -24,9 +24,6 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.translation.GlobalTranslator; -import net.kyori.adventure.translation.TranslationRegistry; -import net.kyori.adventure.translation.Translator; -import org.jetbrains.annotations.Nullable; /** * Velocity Translation Mapper. @@ -43,25 +40,9 @@ public enum TranslatableMapper implements BiConsumer componentConsumer ) { - for (final Translator source : GlobalTranslator.translator().sources()) { - if (source instanceof TranslationRegistry registry - && registry.contains(translatableComponent.key())) { - componentConsumer.accept(GlobalTranslator.render(translatableComponent, - ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); - return; - } - } - final @Nullable String fallback = translatableComponent.fallback(); - if (fallback == null) { - return; - } - for (final Translator source : GlobalTranslator.translator().sources()) { - if (source instanceof TranslationRegistry registry && registry.contains(fallback)) { - componentConsumer.accept( - GlobalTranslator.render(Component.translatable(fallback), - ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); - return; - } + final Locale locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()); + if (GlobalTranslator.translator().canTranslate(translatableComponent.key(), locale)) { + componentConsumer.accept(GlobalTranslator.render(translatableComponent, locale)); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java index d28ab9bf..e878cfd8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java @@ -79,10 +79,10 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { * * @return all legacy channel IDs */ - public Collection getLegacyChannelIds() { - Collection ids = new HashSet<>(); + public Collection getLegacyChannelIds() { + Collection ids = new HashSet<>(); for (ChannelIdentifier value : identifierMap.values()) { - ids.add(value.getId()); + ids.add(new LegacyChannelIdentifier(value.getId())); } return ids; } @@ -92,13 +92,13 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { * * @return the channel IDs for Minecraft 1.13 and above */ - public Collection getModernChannelIds() { - Collection ids = new HashSet<>(); + public Collection getModernChannelIds() { + Collection ids = new HashSet<>(); for (ChannelIdentifier value : identifierMap.values()) { if (value instanceof MinecraftChannelIdentifier) { - ids.add(value.getId()); + ids.add(value); } else { - ids.add(PluginMessageUtil.transformLegacyToModernChannel(value.getId())); + ids.add(MinecraftChannelIdentifier.from(PluginMessageUtil.transformLegacyToModernChannel(value.getId()))); } } return ids; @@ -114,7 +114,7 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { * @param protocolVersion the protocol version in use * @return the list of channels to register */ - public Collection getChannelsForProtocol(ProtocolVersion protocolVersion) { + public Collection getChannelsForProtocol(ProtocolVersion protocolVersion) { if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) { return getModernChannelIds(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java new file mode 100644 index 00000000..692910d5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.util.collect; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ForwardingSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * An unsynchronized collection that puts an upper bound on the size of the collection. + */ +public final class CappedSet extends ForwardingSet { + + private final Set delegate; + private final int upperSize; + + private CappedSet(Set delegate, int upperSize) { + this.delegate = delegate; + this.upperSize = upperSize; + } + + /** + * Creates a capped collection backed by a {@link HashSet}. + * + * @param maxSize the maximum size of the collection + * @param the type of elements in the collection + * @return the new collection + */ + public static Set create(int maxSize) { + return new CappedSet<>(new HashSet<>(), maxSize); + } + + @Override + protected Set delegate() { + return delegate; + } + + @Override + public boolean add(T element) { + if (this.delegate.size() >= upperSize) { + Preconditions.checkState(this.delegate.contains(element), + "collection is too large (%s >= %s)", + this.delegate.size(), this.upperSize); + return false; + } + return this.delegate.add(element); + } + + @Override + public boolean addAll(Collection collection) { + return this.standardAddAll(collection); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java index b42b3ca8..fa745922 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java @@ -22,15 +22,15 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Ticker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import java.net.InetAddress; import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; /** * A simple rate-limiter based on a Caffeine {@link Cache}. */ -public class CaffeineCacheRatelimiter implements Ratelimiter { +public class CaffeineCacheRatelimiter implements Ratelimiter { - private final Cache expiringCache; + private final Cache expiringCache; private final long timeoutNanos; CaffeineCacheRatelimiter(long time, TimeUnit unit) { @@ -49,16 +49,15 @@ public class CaffeineCacheRatelimiter implements Ratelimiter { } /** - * Attempts to rate-limit the client. + * Attempts to rate-limit the object. * - * @param address the address to rate limit - * @return true if we should allow the client, false if we should rate-limit + * @param key the object to rate limit + * @return true if we should allow the object, false if we should rate-limit */ @Override - public boolean attempt(InetAddress address) { - Preconditions.checkNotNull(address, "address"); + public boolean attempt(@NotNull T key) { long expectedNewValue = System.nanoTime() + timeoutNanos; - long last = expiringCache.get(address, (address1) -> expectedNewValue); + long last = expiringCache.get(key, (key1) -> expectedNewValue); return expectedNewValue == last; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java index f420986d..9f77072a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java @@ -17,16 +17,16 @@ package com.velocitypowered.proxy.util.ratelimit; -import java.net.InetAddress; +import org.jetbrains.annotations.NotNull; /** * A {@link Ratelimiter} that does no rate-limiting. */ -enum NoopCacheRatelimiter implements Ratelimiter { +enum NoopCacheRatelimiter implements Ratelimiter { INSTANCE; @Override - public boolean attempt(InetAddress address) { + public boolean attempt(@NotNull Object key) { return true; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java index 973276d4..840b21a1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java @@ -17,18 +17,18 @@ package com.velocitypowered.proxy.util.ratelimit; -import java.net.InetAddress; +import org.jetbrains.annotations.NotNull; /** - * Allows rate limiting of clients. + * Allows rate limiting of objects. */ -public interface Ratelimiter { +public interface Ratelimiter { /** - * Determines whether or not to allow the connection. - * - * @param address the address to rate limit - * @return true if allowed, false if not - */ - boolean attempt(InetAddress address); + * Attempts to rate-limit the object. + * + * @param key the object to rate limit + * @return true if we should allow the object, false if we should rate-limit + */ + boolean attempt(@NotNull T key); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java index 4dafd30c..f3063449 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java @@ -28,8 +28,9 @@ public final class Ratelimiters { throw new AssertionError(); } - public static Ratelimiter createWithMilliseconds(long ms) { - return ms <= 0 ? NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms, + @SuppressWarnings("unchecked") + public static Ratelimiter createWithMilliseconds(long ms) { + return ms <= 0 ? (Ratelimiter) NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms, TimeUnit.MILLISECONDS); } } diff --git a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties index 07aa348f..d56ca9c6 100644 --- a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties +++ b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties @@ -62,4 +62,6 @@ velocity.command.dump-server-error=An error occurred on the Velocity servers and velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection velocity.command.send-usage=/send # Kick -velocity.kick.shutdown=Proxy shutting down. \ No newline at end of file +velocity.kick.shutdown=Proxy shutting down. +velocity.kick.command-rate-limit=You are sending too many commands too quickly. +velocity.kick.tab-complete-rate-limit=You are sending too many tab complete requests too quickly. \ No newline at end of file diff --git a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_bg_BG.properties b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_bg_BG.properties index c50dc10a..b5a12e46 100644 --- a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_bg_BG.properties +++ b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_bg_BG.properties @@ -35,7 +35,7 @@ velocity.command.generic-error=Възникна грешка при изпълн velocity.command.command-does-not-exist=Тази команда не съществува. velocity.command.players-only=Само играчи могат да изпълняват тази команда. velocity.command.server-does-not-exist=Сървър с името {0} не съществува. -velocity.command.player-not-found=Този играч {0} не съществува. +velocity.command.player-not-found=Играч с името {0} не съществува. velocity.command.server-current-server=В момента сте свързан към {0}. velocity.command.server-too-many=Има прекалено много регистрирани сървъри. Използвайте TAB, за да видите всички налични сървъри. velocity.command.server-available=Налични сървъри\: diff --git a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_es_ES.properties b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_es_ES.properties index 910e54cf..a99efb44 100644 --- a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_es_ES.properties +++ b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages_es_ES.properties @@ -29,7 +29,7 @@ velocity.error.modern-forwarding-needs-new-client=Este servidor solo es compatib velocity.error.modern-forwarding-failed=El servidor no ha enviado una solicitud de reenvío al proxy. Asegúrate de que tu servidor está configurado para usar el método de reenvío de Velocity. velocity.error.moved-to-new-server=Has sido echado de {0}\: {1} velocity.error.no-available-servers=No hay servidores disponibles a los que conectarte. Inténtalo de nuevo más tarde o contacta con un administrador. -velocity.error.illegal-chat-characters=Illegal characters in chat +velocity.error.illegal-chat-characters=Caracteres no válidos en el chat # Commands velocity.command.generic-error=Se ha producido un error al ejecutar este comando. velocity.command.command-does-not-exist=Este comando no existe. @@ -60,6 +60,6 @@ velocity.command.dump-success=Se ha creado un informe anónimo que contiene info velocity.command.dump-will-expire=Este enlace caducará en unos días. velocity.command.dump-server-error=Se ha producido un error en los servidores de Velocity y la subida no se ha podido completar. Notifica al equipo de Velocity sobre este problema y proporciona los detalles sobre este error disponibles en el archivo de registro o la consola de tu servidor Velocity. velocity.command.dump-offline=Causa probable\: la configuración DNS del sistema no es válida o no hay conexión a internet -velocity.command.send-usage=/send +velocity.command.send-usage=/send # Kick -velocity.kick.shutdown=Proxy shutting down. \ No newline at end of file +velocity.kick.shutdown=El proxy se ha apagado. \ No newline at end of file diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index ea2c1405..9ed153cc 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -66,6 +66,11 @@ kick-existing-players = false # configuration is used if no servers could be contacted. ping-passthrough = "DISABLED" +# If enabled (default is false), then a sample of the online players on the proxy will be visible +# when hovering over the player count in the server list. +# This doesn't have any effect when ping passthrough is set to either "description" or "all". +sample-players-in-ping = false + # If not enabled (default is true) player IP addresses will be replaced by in logs enable-player-address-logging = true @@ -159,6 +164,33 @@ log-player-connections = true # Transfer packet (Minecraft 1.20.5) to be received. accepts-transfers = false +# Enables support for SO_REUSEPORT. This may help the proxy scale better on multicore systems +# with a lot of incoming connections, and provide better CPU utilization than the existing +# strategy of having a single thread accepting connections and distributing them to worker +# threads. Disabled by default. Requires Linux or macOS. +enable-reuse-port = false + +# How fast (in milliseconds) are clients allowed to send commands after the last command +# By default this is 50ms (20 commands per second) +command-rate-limit = 50 + +# Should we forward commands to the backend upon being rate limited? +# This will forward the command to the server instead of processing it on the proxy. +# Since most server implementations have a rate limit, this will prevent the player +# from being able to send excessive commands to the server. +forward-commands-if-rate-limited = true + +# How many commands are allowed to be sent after the rate limit is hit before the player is kicked? +# Setting this to 0 or lower will disable this feature. +kick-after-rate-limited-commands = 0 + +# How fast (in milliseconds) are clients allowed to send tab completions after the last tab completion +tab-complete-rate-limit = 10 + +# How many tab completions are allowed to be sent after the rate limit is hit before the player is kicked? +# Setting this to 0 or lower will disable this feature. +kick-after-rate-limited-tab-completes = 0 + [query] # Whether to enable responding to GameSpy 4 query responses or not. enabled = false diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java index 504df681..b6fb9cbd 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java @@ -20,8 +20,10 @@ package com.velocitypowered.proxy.util; import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class VelocityChannelRegistrarTest { @@ -46,9 +48,9 @@ class VelocityChannelRegistrarTest { // Two channels cover the modern channel (velocity:test) and the legacy-mapped channel // (legacy:velocitytest). Make sure they're what we expect. assertEquals(ImmutableSet.of(MODERN.getId(), SIMPLE_LEGACY_REMAPPED), registrar - .getModernChannelIds()); + .getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); assertEquals(ImmutableSet.of(SIMPLE_LEGACY.getId(), MODERN.getId()), registrar - .getLegacyChannelIds()); + .getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); } @Test @@ -57,9 +59,10 @@ class VelocityChannelRegistrarTest { registrar.register(SPECIAL_REMAP_LEGACY, MODERN_SPECIAL_REMAP); // This one, just one channel for the modern case. - assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()), registrar.getModernChannelIds()); + assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()), + registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId(), SPECIAL_REMAP_LEGACY.getId()), - registrar.getLegacyChannelIds()); + registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); } @Test @@ -68,7 +71,9 @@ class VelocityChannelRegistrarTest { registrar.register(MODERN, SIMPLE_LEGACY); registrar.unregister(SIMPLE_LEGACY); - assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getModernChannelIds()); - assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getLegacyChannelIds()); + assertEquals(ImmutableSet.of(MODERN.getId()), + registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));; + assertEquals(ImmutableSet.of(MODERN.getId()), + registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); } } \ No newline at end of file diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java new file mode 100644 index 00000000..2e118b4a --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019-2021 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.util.collect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class CappedSetTest { + + @Test + void basicVerification() { + Collection coll = CappedSet.create(1); + assertTrue(coll.add("coffee"), "did not add single item"); + assertThrows(IllegalStateException.class, () -> coll.add("tea"), + "item was added to collection although it is too full"); + assertEquals(1, coll.size(), "collection grew in size unexpectedly"); + } + + @Test + void testAddAll() { + Set doesFill1 = ImmutableSet.of("coffee", "tea"); + Set doesFill2 = ImmutableSet.of("chocolate"); + Set overfill = ImmutableSet.of("Coke", "Pepsi"); + + Collection coll = CappedSet.create(3); + assertTrue(coll.addAll(doesFill1), "did not add items"); + assertTrue(coll.addAll(doesFill2), "did not add items"); + assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), + "items added to collection although it is too full"); + assertEquals(3, coll.size(), "collection grew in size unexpectedly"); + } + + @Test + void handlesSetBehaviorCorrectly() { + Set doesFill1 = ImmutableSet.of("coffee", "tea"); + Set doesFill2 = ImmutableSet.of("coffee", "chocolate"); + Set overfill = ImmutableSet.of("coffee", "Coke", "Pepsi"); + + Collection coll = CappedSet.create(3); + assertTrue(coll.addAll(doesFill1), "did not add items"); + assertTrue(coll.addAll(doesFill2), "did not add items"); + assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), + "items added to collection although it is too full"); + + assertFalse(coll.addAll(doesFill1), "added items?!?"); + + assertEquals(3, coll.size(), "collection grew in size unexpectedly"); + } +} \ No newline at end of file