diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..82cada2b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,69 @@ +name: Bug Report +description: Report issues with Velocity not working properly. +labels: ["type: bug"] +body: + - type: textarea + attributes: + label: Expected Behavior + description: What you expected to work and how. + validations: + required: true + + - type: textarea + attributes: + label: Actual Behavior + description: What actually happens. + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: Information on how we can reproduce this bug on our own, this can be e.g. just an explanation, a video or your Velocity config. + validations: + required: true + + - type: textarea + attributes: + label: Plugin List + description: | + All plugins running on your proxy and the backend server you're experiencing this issue on. + Use `/velocity plugins` to list plugins on Velocity and `/plugins` to list plugins on your backend server. + validations: + required: true + + - type: textarea + attributes: + label: Velocity Version + description: | + The full, unmodified output of running `/velocity info`. + *"Latest"* is not a version. We require you to paste the text, not a screenshot. +
+ Example + + ``` + [17:44:10 INFO]: Velocity 3.3.0-SNAPSHOT (git-9d25d309-b400) + [17:44:10 INFO]: Copyright 2018-2023 Velocity Contributors. Velocity is licensed under the terms of the GNU General Public License v3. + [17:44:10 INFO]: velocitypowered.com - GitHub + ``` +
+ validations: + required: true + + - type: textarea + attributes: + label: Additional Information + description: Anything else you think is helpful. + validations: + required: false + + - type: markdown + attributes: + value: | + Before submitting this issue, please ensure the following: + + 1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity). + 2. You searched for and ensured there isn't already an open issue regarding this. + + If you think you have a bug, but are not sure, feel free to ask in the `#velocity-help` channel on our + [Discord](https://discord.gg/papermc). \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..faf9d2e70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false +contact_links: + - name: PaperMC Discord + url: https://discord.gg/papermc + about: If you are having issues with the proxy not connecting to servers or have other minor issues, come ask us on our Discord server! + - name: Exploit Report + url: https://discord.gg/papermc + about: | + Due to GitHub not currently allowing private issues, exploit reports are currently handled via our Discord. + To report an exploit, see the #paper-exploit-report channel. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..f47b06d00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,48 @@ +name: Feature Request +description: Request for a feature to be implemented into Velocity. +labels: ["type: feature"] +body: + - type: textarea + attributes: + label: Requested Feature + description: | + Please describe as best as you can what you'd like to be added to Velocity. + validations: + required: true + + - type: textarea + attributes: + label: Why is this needed? + description: | + Please describe why do you need this feature. + Do you think it could be useful? Is it due to another problem? + validations: + required: true + + - type: textarea + attributes: + label: Alternative Solutions + description: | + Are there any alternative solutions to implementing a new feature? + What have you tried instead? + validations: + required: true + + - type: textarea + attributes: + label: Additional Information + description: Anything else you want to add. + validations: + required: false + + - type: markdown + attributes: + value: | + Before submitting this request, please ensure the following: + + 1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity). + 2. You searched for and ensured there isn't already an open issue regarding this. + 3. The feature you're requesting has to be implemented on Velocity and not on the backend server. + + If you are unsure whether your problem can already be fixed in another way, feel free to ask in the `#velocity-help` channel on our + [Discord](https://discord.gg/papermc). \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index aa6778fca..2e524db9f 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -67,7 +67,9 @@ tasks { "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/", - "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine" + "https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/", + "https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/", + "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/", ) o.tags( 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 6cf90a791..257fc9e64 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -44,7 +44,7 @@ public interface CommandManager { * @param otherAliases additional aliases * @throws IllegalArgumentException if one of the given aliases is already registered, or * the given command does not implement a registrable {@link Command} subinterface - * @see Command for a list of registrable {@link Command} subinterfaces + * @see Command for a list of registrable Command subinterfaces */ default void register(String alias, Command command, String... otherAliases) { register(metaBuilder(alias).aliases(otherAliases).build(), command); @@ -65,7 +65,7 @@ public interface CommandManager { * @param command the command to register * @throws IllegalArgumentException if one of the given aliases is already registered, or * the given command does not implement a registrable {@link Command} subinterface - * @see Command for a list of registrable {@link Command} subinterfaces + * @see Command for a list of registrable Command subinterfaces */ void register(CommandMeta meta, Command command); diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java new file mode 100644 index 000000000..96d1bb8e5 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.ServerConnection; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This event is fired when the downstream server tries to remove a resource pack from player + * or clear all of them. The proxy will wait on this event to finish before forwarding the + * action to the user. If this event is denied, no resource packs will be removed from player. + */ +@AwaitingEvent +public class ServerResourcePackRemoveEvent implements ResultedEvent { + + private GenericResult result; + private final @MonotonicNonNull UUID packId; + private final ServerConnection serverConnection; + + /** + * Instantiates this event. + */ + public ServerResourcePackRemoveEvent(UUID packId, ServerConnection serverConnection) { + this.result = ResultedEvent.GenericResult.allowed(); + this.packId = packId; + this.serverConnection = serverConnection; + } + + /** + * Returns the id of the resource pack, if it's null all the resource packs + * from player will be cleared. + * + * @return the id + */ + @Nullable + public UUID getPackId() { + return packId; + } + + /** + * Returns the server that tries to remove a resource pack from player or clear all of them. + * + * @return the server connection + */ + public ServerConnection getServerConnection() { + return serverConnection; + } + + @Override + public GenericResult getResult() { + return this.result; + } + + @Override + public void setResult(GenericResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java new file mode 100644 index 000000000..6e042af1c --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when a player entered the configuration state and can be configured by Velocity. + *

Velocity will wait for this event before continuing/ending the configuration state.

+ * + * @param player The player who can be configured. + * @param server The server that is currently configuring the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +@AwaitingEvent +public record PlayerConfigurationEvent(@NotNull Player player, ServerConnection server) { +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java new file mode 100644 index 000000000..05d6c2af0 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when a player is about to enter the configuration state. + * It is not called for the initial configuration of a player after login. + *

Velocity will wait for this event before asking the client to enter configuration state. + * However due to backend server being unable to keep the connection alive during state changes, + * Velocity will only wait for a maximum of 5 seconds.

+ * + * @param player The player who is about to enter configuration state. + * @param server The server that wants to reconfigure the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +@AwaitingEvent +public record PlayerEnterConfigurationEvent(@NotNull Player player, ServerConnection server) { +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java new file mode 100644 index 000000000..c16777066 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.network.ProtocolState; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when a player has entered the configuration state. + *

From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed, + * the {@linkplain Player#getProtocolState()} method is guaranteed + * to return {@link ProtocolState#CONFIGURATION}.

+ * + * @param player The player who has entered the configuration state. + * @param server The server that will now (re-)configure the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +public record PlayerEnteredConfigurationEvent(@NotNull Player player, ServerConnection server) { +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java new file mode 100644 index 000000000..50df5a8ab --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when a player is about to finish the configuration state. + *

Velocity will wait for this event before asking the client to finish the configuration state. + * However due to backend server being unable to keep the connection alive during state changes, + * Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration + * state, use the {@link PlayerConfigurationEvent}.

+ * + * @param player The player who is about to finish the configuration phase. + * @param server The server that has (re-)configured the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +@AwaitingEvent +public record PlayerFinishConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) { +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java new file mode 100644 index 000000000..517f119cf --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.network.ProtocolState; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when a player has finished the configuration state. + *

From this moment on, the {@link Player#getProtocolState()} method + * will return {@link ProtocolState#PLAY}.

+ * + * @param player The player who has finished the configuration state. + * @param server The server that has (re-)configured the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +public record PlayerFinishedConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) { +} 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 new file mode 100644 index 000000000..754492a60 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +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; +import org.jetbrains.annotations.NotNull; + +/** + * This event is fired by the proxy after a backend server is registered to the server map. + * Currently, it may occur when a server is registered dynamically at runtime or when a server is + * replaced due to configuration reload. + * + * @see com.velocitypowered.api.proxy.ProxyServer#registerServer(ServerInfo) + * + * @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 new file mode 100644 index 000000000..36b4023bb --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +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; +import org.jetbrains.annotations.NotNull; + +/** + * This event is fired by the proxy after a backend server is unregistered from the server map. + * Currently, it may occur when a server is unregistered dynamically at runtime + * or when a server is replaced due to configuration reload. + * + * @see com.velocitypowered.api.proxy.ProxyServer#unregisterServer(ServerInfo) + * + * @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 e968b0255..8bb0c98b0 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -87,7 +87,7 @@ public enum ProtocolVersion implements Ordered { MINECRAFT_1_20_2(764, "1.20.2"), MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"), MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), - MINECRAFT_1_21(767, "1.21"); + MINECRAFT_1_21(767, "1.21", "1.21.1"); private static final int SNAPSHOT_BIT = 30; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index dfe9a2bc7..04e65c849 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.api.util.ServerLink; import java.net.InetSocketAddress; import java.util.Collection; import java.util.List; @@ -461,4 +462,16 @@ public interface Player extends * @sinceMinecraft 1.20.5 */ void requestCookie(Key key); + + /** + * Send the player a list of custom links to display in their client's pause menu. + * + *

Note that later packets sent by the backend server may override links sent by the proxy. + * + * @param links an ordered list of {@link ServerLink}s to send to the player + * @throws IllegalArgumentException if the player is from a version lower than 1.21 + * @since 3.3.0 + * @sinceMinecraft 1.21 + */ + void setServerLinks(@NotNull List links); } \ No newline at end of file diff --git a/api/src/main/java/com/velocitypowered/api/util/ServerLink.java b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java new file mode 100644 index 000000000..9eb04a980 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021-2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.util; + +import com.google.common.base.Preconditions; +import java.net.URI; +import java.util.Optional; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a custom URL servers can show in player pause menus. + * Links can be of a built-in type or use a custom component text label. + */ +public final class ServerLink { + + private @Nullable Type type; + private @Nullable Component label; + private final URI url; + + private ServerLink(Component label, String url) { + this.label = Preconditions.checkNotNull(label, "label"); + this.url = URI.create(url); + } + + private ServerLink(Type type, String url) { + this.type = Preconditions.checkNotNull(type, "type"); + this.url = URI.create(url); + } + + /** + * Construct a server link with a custom component label. + * + * @param label a custom component label to display + * @param link the URL to open when clicked + */ + public static ServerLink serverLink(Component label, String link) { + return new ServerLink(label, link); + } + + /** + * Construct a server link with a built-in type. + * + * @param type the {@link Type built-in type} of link + * @param link the URL to open when clicked + */ + public static ServerLink serverLink(Type type, String link) { + return new ServerLink(type, link); + } + + /** + * Get the type of the server link. + * + * @return the type of the server link + */ + public Optional getBuiltInType() { + return Optional.ofNullable(type); + } + + /** + * Get the custom component label of the server link. + * + * @return the custom component label of the server link + */ + public Optional getCustomLabel() { + return Optional.ofNullable(label); + } + + /** + * Get the link {@link URI}. + * + * @return the link {@link URI} + */ + public URI getUrl() { + return url; + } + + /** + * Built-in types of server links. + * + * @apiNote {@link Type#BUG_REPORT} links are shown on the connection error screen + */ + public enum Type { + BUG_REPORT, + COMMUNITY_GUIDELINES, + SUPPORT, + STATUS, + FEEDBACK, + COMMUNITY, + WEBSITE, + FORUMS, + NEWS, + ANNOUNCEMENTS + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2d6ab114..35aa6910d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ shadow = "io.github.goooler.shadow:8.1.5" spotless = "com.diffplug.spotless:6.25.0" [libraries] -adventure-bom = "net.kyori:adventure-bom:4.16.0" +adventure-bom = "net.kyori:adventure-bom:4.17.0" adventure-facet = "net.kyori:adventure-platform-facet:4.3.2" asm = "org.ow2.asm:asm:9.6" auto-service = "com.google.auto.service:auto-service:1.0.1" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index ef8217c69..004243616 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -45,7 +45,7 @@ import com.velocitypowered.proxy.command.builtin.ShutdownCommand; import com.velocitypowered.proxy.command.builtin.VelocityCommand; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ServerListPingHandler; import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.crypto.EncryptionUtils; 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 7904a06a0..79ac41e74 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 @@ -379,7 +379,7 @@ public final class VelocityCommand { this.heapGenerator.invoke(hotspotMbean, file.toString(), true); } catch (Throwable e1) { // This should not occur - throw new RuntimeException(e); + throw new RuntimeException(e1); } src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN)); }; 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 11c65d527..9c2e1e828 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -60,7 +60,7 @@ public class VelocityConfiguration implements ProxyConfig { private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class); @Expose - private String bind = "0.0.0.0:25577"; + private String bind = "0.0.0.0:25565"; @Expose private String motd = "A Velocity Server"; @Expose @@ -503,7 +503,7 @@ public class VelocityConfiguration implements ProxyConfig { final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", PingPassthroughMode.DISABLED); - final String bind = config.getOrElse("bind", "0.0.0.0:25577"); + 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); final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true); @@ -830,7 +830,7 @@ public class VelocityConfiguration implements ProxyConfig { @Expose private boolean queryEnabled = false; @Expose - private int queryPort = 25577; + private int queryPort = 25565; @Expose private String queryMap = "Velocity"; @Expose @@ -849,7 +849,7 @@ public class VelocityConfiguration implements ProxyConfig { private Query(CommentedConfig config) { if (config != null) { this.queryEnabled = config.getOrElse("enabled", false); - this.queryPort = config.getIntOrElse("port", 25577); + this.queryPort = config.getIntOrElse("port", 25565); this.queryMap = config.getOrElse("map", "Velocity"); this.showPlugins = config.getOrElse("show-plugins", false); } 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 b1c91ebdb..c91739078 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -47,7 +47,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; -import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueInboundHandler; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; @@ -148,13 +149,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { return; } - if (msg instanceof MinecraftPacket) { - MinecraftPacket pkt = (MinecraftPacket) msg; + if (msg instanceof MinecraftPacket pkt) { if (!pkt.handle(activeSessionHandler)) { activeSessionHandler.handleGeneric((MinecraftPacket) msg); } - } else if (msg instanceof HAProxyMessage) { - HAProxyMessage proxyMessage = (HAProxyMessage) msg; + } else if (msg instanceof HAProxyMessage proxyMessage) { this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); } else if (msg instanceof ByteBuf) { @@ -383,9 +382,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (state == StateRegistry.CONFIG) { // Activate the play packet queue addPlayPacketQueueHandler(); - } else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) { + } else { // Remove the queue - this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE); + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) { + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_OUTBOUND); + } + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) != null) { + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_INBOUND); + } } } @@ -393,10 +397,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { * Adds the play packet queue handler. */ public void addPlayPacketQueueHandler() { - if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { - this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, - new PlayPacketQueueHandler(this.protocolVersion, - channel.pipeline().get(MinecraftEncoder.class).getDirection())); + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND, + new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection())); + } + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND, + new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection())); } } 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 5b83e6549..98b1b3e6e 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 @@ -29,10 +29,13 @@ import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieStoreEvent; 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.event.proxy.ProxyPingEvent; 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.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.command.CommandGraphInjector; @@ -40,8 +43,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; -import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -258,14 +261,26 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(RemoveResourcePackPacket packet) { - final ConnectedPlayer player = serverConn.getPlayer(); - final ResourcePackHandler handler = player.resourcePackHandler(); - if (packet.getId() != null) { - handler.remove(packet.getId()); - } else { - handler.clearAppliedResourcePacks(); - } - playerConnection.write(packet); + final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent( + packet.getId(), this.serverConn); + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackRemoveEvent.getResult().isAllowed()) { + final ConnectedPlayer player = serverConn.getPlayer(); + final ResourcePackHandler handler = player.resourcePackHandler(); + if (packet.getId() != null) { + handler.remove(packet.getId()); + } else { + handler.clearAppliedResourcePacks(); + } + playerConnection.write(packet); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling resource pack remove for {}", playerConnection, ex); + return null; + }); return true; } @@ -275,31 +290,14 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } - // Register and unregister packets are simply forwarded to the server as-is. - if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) { - return false; - } - - if (PluginMessageUtil.isMcBrand(packet)) { - PluginMessagePacket rewritten = PluginMessageUtil - .rewriteMinecraftBrand(packet, - server.getVersion(), playerConnection.getProtocolVersion()); - playerConnection.write(rewritten); - return true; - } - if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) { // Handled. return true; } - ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); - if (id == null) { - return false; - } - byte[] copy = ByteBufUtil.getBytes(packet.content()); - PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy); + String channel = packet.getChannel(); + PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy); server.getEventManager().fire(event).thenAcceptAsync(pme -> { if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { PluginMessagePacket copied = new PluginMessagePacket( 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 3c2691781..b047d1868 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 @@ -37,7 +37,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.util.Optional; import java.util.StringJoiner; -import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.ComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -143,7 +142,7 @@ public class BungeeCordMessageResponder { out.writeUTF("PlayerList"); out.writeUTF(info.getServerInfo().getName()); - StringJoiner joiner = new StringJoiner(", "); + final StringJoiner joiner = new StringJoiner(", "); for (Player online : info.getPlayersConnected()) { joiner.add(online.getUsername()); } @@ -187,10 +186,9 @@ public class BungeeCordMessageResponder { Component messageComponent = serializer.deserialize(message); if (target.equals("ALL")) { - proxy.sendMessage(Identity.nil(), messageComponent); + proxy.sendMessage(messageComponent); } else { - proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(), - messageComponent)); + proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent)); } } @@ -262,6 +260,13 @@ public class BungeeCordMessageResponder { }); } + private void processKickRaw(ByteBufDataInput in) { + proxy.getPlayer(in.readUTF()).ifPresent(player -> { + String kickReason = in.readUTF(); + player.disconnect(GsonComponentSerializer.gson().deserialize(kickReason)); + }); + } + private void processForwardToPlayer(ByteBufDataInput in) { Optional player = proxy.getPlayer(in.readUTF()); if (player.isPresent()) { @@ -374,6 +379,9 @@ public class BungeeCordMessageResponder { case "KickPlayer": this.processKick(in); break; + case "KickPlayerRaw": + this.processKickRaw(in); + break; default: // Do nothing, unknown command break; 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 3f4325e52..74f0576c1 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 @@ -21,6 +21,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieStoreEvent; 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.player.ResourcePackInfo; @@ -29,7 +30,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -41,6 +43,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; +import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket; @@ -132,7 +135,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KeepAlivePacket packet) { - serverConn.ensureConnected().write(packet); + serverConn.getPendingPings().put(packet.getRandomId(), System.nanoTime()); + serverConn.getPlayer().getConnection().write(packet); return true; } @@ -192,31 +196,53 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { } @Override - public boolean handle(FinishedUpdatePacket packet) { - MinecraftConnection smc = serverConn.ensureConnected(); - ConnectedPlayer player = serverConn.getPlayer(); - ClientConfigSessionHandler configHandler = - (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); + public boolean handle(RemoveResourcePackPacket packet) { + final MinecraftConnection playerConnection = this.serverConn.getPlayer().getConnection(); + + final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent( + packet.getId(), this.serverConn); + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackRemoveEvent.getResult().isAllowed()) { + final ConnectedPlayer player = serverConn.getPlayer(); + final ResourcePackHandler handler = player.resourcePackHandler(); + if (packet.getId() != null) { + handler.remove(packet.getId()); + } else { + handler.clearAppliedResourcePacks(); + } + playerConnection.write(packet); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling resource pack remove for {}", playerConnection, ex); + return null; + }); + return true; + } + + @Override + public boolean handle(FinishedUpdatePacket packet) { + final MinecraftConnection smc = serverConn.ensureConnected(); + final ConnectedPlayer player = serverConn.getPlayer(); + final ClientConfigSessionHandler configHandler = (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); - smc.setAutoReading(false); - // Even when not auto reading messages are still decoded. Decode them with the correct state smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); - configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { + //noinspection DataFlowIssue + configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> { + smc.write(FinishedUpdatePacket.INSTANCE); if (serverConn == player.getConnectedServer()) { smc.setActiveSessionHandler(StateRegistry.PLAY); - player.sendPlayerListHeaderAndFooter( - player.getPlayerListHeader(), player.getPlayerListFooter()); + player.sendPlayerListHeaderAndFooter(player.getPlayerListHeader(), player.getPlayerListFooter()); // The client cleared the tab list. TODO: Restore changes done via TabList API player.getTabList().clearAllSilent(); } else { - smc.setActiveSessionHandler(StateRegistry.PLAY, - new TransitionSessionHandler(server, serverConn, resultFuture)); + smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } - if (player.resourcePackHandler().getFirstAppliedPack() == null - && resourcePackToApply != null) { + if (player.resourcePackHandler().getFirstAppliedPack() == null && resourcePackToApply != null) { player.resourcePackHandler().queueResourcePack(resourcePackToApply); } - smc.setAutoReading(true); }, smc.eventLoop()); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index a672c9174..612e9c25a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; @@ -142,10 +143,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerLoginSuccessPacket packet) { - if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN - && !informationForwarded) { - resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, - serverConn.getServer())); + if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) { + resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer())); serverConn.disconnect(); return true; } @@ -156,22 +155,20 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // Move into the PLAY phase. MinecraftConnection smc = serverConn.ensureConnected(); if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { - smc.setActiveSessionHandler(StateRegistry.PLAY, - new TransitionSessionHandler(server, serverConn, resultFuture)); + smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } else { smc.write(new LoginAcknowledgedPacket()); - smc.setActiveSessionHandler(StateRegistry.CONFIG, - new ConfigSessionHandler(server, serverConn, resultFuture)); + smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture)); ConnectedPlayer player = serverConn.getPlayer(); if (player.getClientSettingsPacket() != null) { smc.write(player.getClientSettingsPacket()); } - if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) { smc.setAutoReading(false); - ((ClientPlaySessionHandler) player.getConnection() - .getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> { - smc.setAutoReading(true); - }, smc.eventLoop()); + clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop()); + } else { + // Initial login - the player is already in configuration state. + server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn)); } } @@ -211,7 +208,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { The connection to the remote server was unexpectedly closed. This is usually because the remote server does not have \ BungeeCord IP forwarding correctly enabled. - See https://velocitypowered.com/wiki/users/forwarding/ for instructions \ + See https://docs.papermc.io/velocity/player-information-forwarding for instructions \ on how to configure player info forwarding correctly.""")); } else { resultFuture.completeExceptionally( 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 77290584d..ac02bcf76 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 @@ -178,14 +178,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler { inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); } else { loginState = State.ACKNOWLEDGED; - mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, - new ClientConfigSessionHandler(server, connectedPlayer)); + mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, connectedPlayer)); - server.getEventManager().fire(new PostLoginEvent(connectedPlayer)) - .thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); - return null; - }); + server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> { + return connectToInitialServer(connectedPlayer); + }).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); + return null; + }); } return true; } @@ -224,8 +224,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { player.disconnect0(reason.get(), true); } else { if (!server.registerConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), - true); + player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), true); return; } @@ -238,13 +237,13 @@ public class AuthSessionHandler implements MinecraftSessionHandler { loginState = State.SUCCESS_SENT; if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { loginState = State.ACKNOWLEDGED; - mcConnection.setActiveSessionHandler(StateRegistry.PLAY, - new InitialConnectSessionHandler(player, server)); - server.getEventManager().fire(new PostLoginEvent(player)) - .thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", player, ex); - return null; - }); + mcConnection.setActiveSessionHandler(StateRegistry.PLAY, new InitialConnectSessionHandler(player, server)); + server.getEventManager().fire(new PostLoginEvent(player)).thenCompose((ignored) -> { + return connectToInitialServer(player); + }).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); } } }, mcConnection.eventLoop()).exceptionally((ex) -> { 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 52b67f7b5..7bb7bedfa 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 @@ -19,6 +19,9 @@ package com.velocitypowered.proxy.connection.client; 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.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; @@ -46,8 +49,6 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * Handles the client config stage. @@ -59,6 +60,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { private final ConnectedPlayer player; private String brandChannel = null; + private CompletableFuture configurationFuture; private CompletableFuture configSwitchFuture; /** @@ -77,13 +79,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { configSwitchFuture = new CompletableFuture<>(); } + @Override + public void deactivated() { + configurationFuture = null; + } + @Override public boolean handle(final KeepAlivePacket packet) { - final VelocityServerConnection serverConnection = player.getConnectedServer(); - if (!this.sendKeepAliveToBackend(serverConnection, packet)) { - final VelocityServerConnection connectionInFlight = player.getConnectionInFlight(); - this.sendKeepAliveToBackend(connectionInFlight, packet); - } + player.forwardKeepAlive(packet); return true; } @@ -104,8 +107,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(FinishedUpdatePacket packet) { - player.getConnection() - .setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); configSwitchFuture.complete(null); return true; @@ -139,12 +141,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KnownPacksPacket packet) { - if (player.getConnectionInFlight() != null) { - player.getConnectionInFlight().ensureConnected().write(packet); - return true; - } + callConfigurationEvent().thenRun(() -> { + player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet); + }).exceptionally(ex -> { + logger.error("Error forwarding known packs response to backend:", ex); + return null; + }); - return false; + return true; } @Override @@ -207,26 +211,25 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public void exception(Throwable throwable) { - player.disconnect( - Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); + player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); } - private boolean sendKeepAliveToBackend( - final @Nullable VelocityServerConnection serverConnection, - final @NotNull KeepAlivePacket packet - ) { - if (serverConnection != null) { - final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); - if (sentTime != null) { - final MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null) { - player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); - smc.write(packet); - return true; - } - } + /** + * Calls the {@link PlayerConfigurationEvent}. + * For 1.20.5+ backends this is done when the client responds to + * the known packs request. The response is delayed until the event + * has been called. + * For 1.20.2-1.20.4 servers this is done when the client acknowledges + * the end of the configuration. + * This is handled differently because for 1.20.5+ servers can't keep + * their connection alive between states and older servers don't have + * the known packs transaction. + */ + private CompletableFuture callConfigurationEvent() { + if (configurationFuture != null) { + return configurationFuture; } - return false; + return configurationFuture = server.getEventManager().fire(new PlayerConfigurationEvent(player, player.getConnectionInFlightOrConnectedServer())); } /** @@ -246,14 +249,18 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { smc.write(brandPacket); } - player.getConnection().eventLoop().execute(() -> { + callConfigurationEvent().thenCompose(v -> { + return server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn)) + .completeOnTimeout(null, 5, TimeUnit.SECONDS); + }).thenRunAsync(() -> { player.getConnection().write(FinishedUpdatePacket.INSTANCE); player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); + server.getEventManager().fireAndForget(new PlayerFinishedConfigurationEvent(player, serverConn)); + }, player.getConnection().eventLoop()).exceptionally(ex -> { + logger.error("Error finishing configuration state:", ex); + return null; }); - smc.write(FinishedUpdatePacket.INSTANCE); - smc.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); - return configSwitchFuture; } } 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 2a595b167..3b3f7d422 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 @@ -27,6 +27,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; 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; @@ -53,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler; import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; @@ -84,7 +86,6 @@ import java.util.Queue; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -176,17 +177,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KeepAlivePacket packet) { - final VelocityServerConnection serverConnection = player.getConnectedServer(); - if (serverConnection != null) { - final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); - if (sentTime != null) { - final MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null) { - player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); - smc.write(packet); - } - } - } + player.forwardKeepAlive(packet); return true; } @@ -349,43 +340,25 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } if (!player.getPhase().handle(player, packet, serverConn)) { - ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); - if (id == null) { - // We don't have any plugins listening on this channel, process the packet now. - if (!player.getPhase().consideredComplete() || !serverConn.getPhase() - .consideredComplete()) { - // The client is trying to send messages too early. This is primarily caused by mods, - // but further aggravated by Velocity. To work around these issues, we will queue any - // non-FML handshake messages to be sent once the FML handshake has completed or the - // JoinGame packet has been received by the proxy, whichever comes first. - // - // We also need to make sure to retain these packets, so they can be flushed - // appropriately. - loginPluginMessages.add(packet.retain()); - } else { - // The connection is ready, send the packet now. - backendConn.write(packet.retain()); - } - } else { - byte[] copy = ByteBufUtil.getBytes(packet.content()); - PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy); - server.getEventManager().fire(event).thenAcceptAsync(pme -> { - if (pme.getResult().isAllowed()) { - PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(), - Unpooled.wrappedBuffer(copy)); - if (!player.getPhase().consideredComplete() || !serverConn.getPhase() - .consideredComplete()) { - // We're still processing the connection (see above), enqueue the packet for now. - loginPluginMessages.add(message.retain()); - } else { - backendConn.write(message); - } + byte[] copy = ByteBufUtil.getBytes(packet.content()); + String channel = packet.getChannel(); + PluginMessageEvent event = new PluginMessageEvent(player, serverConn, channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy); + server.getEventManager().fire(event).thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed()) { + PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(), + Unpooled.wrappedBuffer(copy)); + if (!player.getPhase().consideredComplete() || !serverConn.getPhase() + .consideredComplete()) { + // We're still processing the connection (see above), enqueue the packet for now. + loginPluginMessages.add(message.retain()); + } else { + backendConn.write(message); } - }, backendConn.eventLoop()).exceptionally((ex) -> { - logger.error("Exception while handling plugin message packet for {}", player, ex); - return null; - }); - } + } + }, backendConn.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message packet for {}", player, ex); + return null; + }); } } } @@ -406,6 +379,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Complete client switch player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); VelocityServerConnection serverConnection = player.getConnectedServer(); + server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); if (serverConnection != null) { MinecraftConnection smc = serverConnection.ensureConnected(); CompletableFuture.runAsync(() -> { @@ -421,6 +395,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(ChatAcknowledgementPacket packet) { + if (player.getCurrentServer().isEmpty()) { + return true; + } + player.getChatQueue().handleAcknowledgement(packet.offset()); + return true; + } + @Override public boolean handle(ServerboundCookieResponsePacket packet) { server.getEventManager() @@ -512,7 +495,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { * @return a future that completes when the switch is complete */ public CompletableFuture doSwitch() { - VelocityServerConnection existingConnection = player.getConnectedServer(); + final VelocityServerConnection existingConnection = player.getConnectedServer(); if (existingConnection != null) { // Shut down the existing server connection. 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 a471e2b53..2b22fc7bc 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 @@ -37,6 +37,7 @@ import com.velocitypowered.api.event.player.KickedFromServerEvent.ServerKickResu import com.velocitypowered.api.event.player.PlayerModInfoEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnterConfigurationEvent; import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; @@ -54,18 +55,21 @@ import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; -import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler; +import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; @@ -80,7 +84,9 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatType; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; +import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket; +import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; @@ -106,6 +112,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.identity.Identity; @@ -629,6 +636,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return connectionInFlight; } + public VelocityServerConnection getConnectionInFlightOrConnectedServer() { + return connectionInFlight != null ? connectionInFlight : connectedServer; + } + public void resetInFlightConnection() { connectionInFlight = null; } @@ -806,7 +817,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, }, connection.eventLoop()); } else if (event.getResult() instanceof final Notify res) { if (event.kickedDuringServerConnect() && previousConnection != null) { - sendMessage(Identity.nil(), res.getMessageComponent()); + sendMessage(res.getMessageComponent()); } else { disconnect(res.getMessageComponent()); } @@ -1057,6 +1068,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, }, connection.eventLoop()); } + @Override + public void setServerLinks(final @NotNull List links) { + Preconditions.checkNotNull(links, "links"); + Preconditions.checkArgument( + this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21), + "Player version must be at least 1.21 to be able to set server links"); + + if (connection.getState() != StateRegistry.PLAY + && connection.getState() != StateRegistry.CONFIG) { + 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())); + } + @Override public void addCustomChatCompletions(@NotNull Collection completions) { Preconditions.checkNotNull(completions, "completions"); @@ -1088,11 +1115,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, "input cannot be greater than " + LegacyChatPacket.MAX_SERVERBOUND_MESSAGE_LENGTH + " characters in length"); if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) { - this.chatQueue.hijack(getChatBuilderFactory().builder().asPlayer(this).message(input), - (instant, item) -> { - item.setTimestamp(instant); - return item.toServer(); - }); + ChatBuilderV2 message = getChatBuilderFactory().builder().asPlayer(this).message(input); + this.chatQueue.queuePacket(chatState -> { + message.setTimestamp(chatState.lastTimestamp); + message.setLastSeenMessages(chatState.createLastSeen()); + return message.toServer(); + }); } else { ensureBackendConnection().write(getChatBuilderFactory().builder() .asPlayer(this).message(input).toServer()); @@ -1217,20 +1245,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } } + /** + * Forwards the keep alive packet to the backend server it belongs to. + * This is either the connection in flight or the connected server. + */ + public boolean forwardKeepAlive(final KeepAlivePacket packet) { + if (!this.sendKeepAliveToBackend(connectedServer, packet)) { + return this.sendKeepAliveToBackend(connectionInFlight, packet); + } + return false; + } + + private boolean sendKeepAliveToBackend(final @Nullable VelocityServerConnection serverConnection, final @NotNull KeepAlivePacket packet) { + if (serverConnection != null) { + final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + final MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); + smc.write(packet); + return true; + } + } + } + return false; + } + /** * Switches the connection to the client into config state. */ public void switchToConfigState() { - CompletableFuture.runAsync(() -> { - connection.write(StartUpdatePacket.INSTANCE); - 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(); - }, connection.eventLoop()).exceptionally((ex) -> { - logger.error("Error switching player connection to config state:", ex); - return null; - }); + server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer())) + .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> { + if (bundleHandler.isInBundleSession()) { + bundleHandler.toggleBundleSession(); + connection.write(BundleDelimiterPacket.INSTANCE); + } + connection.write(StartUpdatePacket.INSTANCE); + 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(); + }, connection.eventLoop()).exceptionally((ex) -> { + logger.error("Error switching player connection to config state", ex); + return null; + }); } /** @@ -1363,24 +1421,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } switch (status.getStatus()) { - case ALREADY_CONNECTED: - sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); - break; - case CONNECTION_IN_PROGRESS: - sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); - break; - case CONNECTION_CANCELLED: + case ALREADY_CONNECTED -> sendMessage(ConnectionMessages.ALREADY_CONNECTED); + case CONNECTION_IN_PROGRESS -> sendMessage(ConnectionMessages.IN_PROGRESS); + case CONNECTION_CANCELLED -> { // Ignored; the plugin probably already handled this. - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + } + case SERVER_DISCONNECTED -> { + final Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); handleConnectionException(toConnect, - DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe()); - break; - default: + DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe()); + } + default -> { // The only remaining value is successful (no need to do anything!) - break; + } } }, connection.eventLoop()).thenApply(Result::isSuccessful); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java similarity index 95% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java index e92d10587..d5d52ef03 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java @@ -15,10 +15,11 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.client; +package com.velocitypowered.proxy.connection.player.bundle; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import java.util.concurrent.CompletableFuture; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java index 67f90fd15..4292710e0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player; +package com.velocitypowered.proxy.connection.player.resourcepack; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.player.ResourcePackInfo; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java similarity index 99% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java index 385bdce4c..a1af567dd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.proxy.VelocityServer; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java index b1b325484..53fa2421c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java @@ -15,13 +15,14 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java index 5ba74a273..077ce701d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; @@ -23,6 +23,7 @@ import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java similarity index 97% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java index 4e6e72505..e5df9f659 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java @@ -15,14 +15,15 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index 27ec4ba8b..ca4e6b1b2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -35,7 +35,8 @@ public class Connections { public static final String MINECRAFT_DECODER = "minecraft-decoder"; public static final String MINECRAFT_ENCODER = "minecraft-encoder"; public static final String READ_TIMEOUT = "read-timeout"; - public static final String PLAY_PACKET_QUEUE = "play-packet-queue"; + public static final String PLAY_PACKET_QUEUE_OUTBOUND = "play-packet-queue-outbound"; + public static final String PLAY_PACKET_QUEUE_INBOUND = "play-packet-queue-inbound"; private Connections() { throw new AssertionError(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 2c6f752f7..0af477c42 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -43,13 +43,13 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -86,35 +86,48 @@ public class VelocityPluginManager implements PluginManager { checkNotNull(directory, "directory"); checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory"); - List found = new ArrayList<>(); + Map foundCandidates = new LinkedHashMap<>(); JavaPluginLoader loader = new JavaPluginLoader(server, directory); try (DirectoryStream stream = Files.newDirectoryStream(directory, p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { for (Path path : stream) { try { - found.add(loader.loadCandidate(path)); + PluginDescription candidate = loader.loadCandidate(path); + + // If we found a duplicate candidate (with the same ID), don't load it. + PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( + candidate.getId(), candidate); + + if (maybeExistingCandidate != null) { + logger.error("Refusing to load plugin at path {} since we already " + + "loaded a plugin with the same ID {} from {}", + candidate.getSource().map(Objects::toString).orElse(""), + candidate.getId(), + maybeExistingCandidate.getSource().map(Objects::toString).orElse("")); + } } catch (Throwable e) { logger.error("Unable to load plugin {}", path, e); } } } - if (found.isEmpty()) { + if (foundCandidates.isEmpty()) { // No plugins found return; } - List sortedPlugins = PluginDependencyUtils.sortCandidates(found); + List sortedPlugins = PluginDependencyUtils.sortCandidates( + new ArrayList<>(foundCandidates.values())); - Set loadedPluginsById = new HashSet<>(); + Map loadedCandidates = new HashMap<>(); Map pluginContainers = new LinkedHashMap<>(); // Now load the plugins pluginLoad: for (PluginDescription candidate : sortedPlugins) { // Verify dependencies for (PluginDependency dependency : candidate.getDependencies()) { - if (!dependency.isOptional() && !loadedPluginsById.contains(dependency.getId())) { + if (!dependency.isOptional() && !loadedCandidates.containsKey(dependency.getId())) { logger.error("Can't load plugin {} due to missing dependency {}", candidate.getId(), dependency.getId()); continue pluginLoad; @@ -125,7 +138,7 @@ public class VelocityPluginManager implements PluginManager { PluginDescription realPlugin = loader.createPluginFromCandidate(candidate); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); pluginContainers.put(container, loader.createModule(container)); - loadedPluginsById.add(realPlugin.getId()); + loadedCandidates.put(realPlugin.getId(), realPlugin); } catch (Throwable e) { logger.error("Can't create module for plugin {}", candidate.getId(), e); } 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 f8362cc09..d75cb46ca 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 @@ -56,8 +56,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof ByteBuf) { - ByteBuf buf = (ByteBuf) msg; + if (msg instanceof ByteBuf buf) { tryDecode(ctx, buf); } else { ctx.fireChannelRead(msg); @@ -147,4 +146,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { this.state = state; this.setProtocolVersion(registry.version); } + + public ProtocolUtils.Direction getDirection() { + return direction; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java new file mode 100644 index 000000000..1affc13bc --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 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.protocol.netty; + +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 io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.ReferenceCountUtil; +import java.util.ArrayDeque; +import java.util.Queue; +import org.jetbrains.annotations.NotNull; + +/** + * Queues up any pending PLAY packets while the client is in the CONFIG state. + * + *

Much of the Velocity API (i.e. chat messages) utilize PLAY packets, however the client is + * incapable of receiving these packets during the CONFIG state. Certain events such as the + * ServerPreConnectEvent may be called during this time, and we need to ensure that any API that + * uses these packets will work as expected. + * + *

This handler will queue up any packets that are sent to the client during this time, and send + * them once the client has (re)entered the PLAY state. + */ +public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler { + + private final StateRegistry.PacketRegistry.ProtocolRegistry registry; + private final Queue queue = new ArrayDeque<>(); + + /** + * Provides registries for client & server bound packets. + * + * @param version the protocol version + */ + public PlayPacketQueueInboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof final MinecraftPacket packet) { + // If the packet exists in the CONFIG state, we want to always + // ensure that it gets handled by the current handler + if (this.registry.containsPacket(packet)) { + ctx.fireChannelRead(msg); + return; + } + } + + // Otherwise, queue the packet + this.queue.offer(msg); + } + + @Override + public void channelInactive(@NotNull ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, false); + + super.channelInactive(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, ctx.channel().isActive()); + } + + private void releaseQueue(ChannelHandlerContext ctx, boolean active) { + // Handle all the queued packets + Object msg; + while ((msg = this.queue.poll()) != null) { + if (active) { + ctx.fireChannelRead(msg); + } else { + ReferenceCountUtil.release(msg); + } + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java similarity index 82% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java index 990985c25..c57271040 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java @@ -25,7 +25,7 @@ import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.PlatformDependent; +import java.util.ArrayDeque; import java.util.Queue; import org.jetbrains.annotations.NotNull; @@ -40,38 +40,36 @@ import org.jetbrains.annotations.NotNull; *

This handler will queue up any packets that are sent to the client during this time, and send * them once the client has (re)entered the PLAY state. */ -public class PlayPacketQueueHandler extends ChannelDuplexHandler { +public class PlayPacketQueueOutboundHandler extends ChannelDuplexHandler { private final StateRegistry.PacketRegistry.ProtocolRegistry registry; - private final Queue queue = PlatformDependent.newMpscQueue(); + private final Queue queue = new ArrayDeque<>(); /** * Provides registries for client & server bound packets. * * @param version the protocol version */ - public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { - this.registry = - StateRegistry.CONFIG.getProtocolRegistry(direction, version); + public PlayPacketQueueOutboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version); } @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) - throws Exception { - if (!(msg instanceof MinecraftPacket)) { + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof final MinecraftPacket packet)) { ctx.write(msg, promise); return; } // If the packet exists in the CONFIG state, we want to always // ensure that it gets sent out to the client - if (this.registry.containsPacket(((MinecraftPacket) msg))) { + if (this.registry.containsPacket(packet)) { ctx.write(msg, promise); return; } // Otherwise, queue the packet - this.queue.offer((MinecraftPacket) msg); + this.queue.offer(packet); } @Override @@ -87,10 +85,6 @@ public class PlayPacketQueueHandler extends ChannelDuplexHandler { } private void releaseQueue(ChannelHandlerContext ctx, boolean active) { - if (this.queue.isEmpty()) { - return; - } - // Send out all the queued packets MinecraftPacket packet; while ((packet = this.queue.poll()) != null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java index 07f9f776a..a0f86aed1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java @@ -21,7 +21,7 @@ import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java index 6ca648a73..b0718090e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java @@ -54,4 +54,8 @@ public class ChatAcknowledgementPacket implements MinecraftPacket { "offset=" + offset + '}'; } + + public int offset() { + return offset; + } } 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 2ddcff17d..5928bf36f 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 @@ -23,7 +23,9 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import io.netty.channel.ChannelFuture; import org.checkerframework.checker.nullness.qual.Nullable; import java.time.Instant; +import java.util.BitSet; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; /** @@ -32,9 +34,10 @@ import java.util.function.Function; */ public class ChatQueue { - private final Object internalLock; + private final Object internalLock = new Object(); private final ConnectedPlayer player; - private CompletableFuture packetFuture; + private final ChatState chatState = new ChatState(); + private CompletableFuture head = CompletableFuture.completedFuture(null); /** * Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}. @@ -43,8 +46,19 @@ public class ChatQueue { */ public ChatQueue(ConnectedPlayer player) { this.player = player; - this.packetFuture = CompletableFuture.completedFuture(new WrappedPacket(Instant.EPOCH, null)); - this.internalLock = new Object(); + } + + private void queueTask(Task task) { + synchronized (internalLock) { + MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); + head = head.thenCompose(v -> { + try { + return task.update(chatState, smc).exceptionally(ignored -> null); + } catch (Throwable ignored) { + return CompletableFuture.completedFuture(null); + } + }); + } } /** @@ -52,121 +66,115 @@ public class ChatQueue { * packets. This maintains order on the server-level for the client insertions of commands * and messages. All entries are locked through an internal object lock. * - * @param nextPacket the {@link CompletableFuture} which will provide the next-processed packet. - * @param timestamp the {@link Instant} timestamp of this packet so we can allow piggybacking. + * @param nextPacket a function mapping {@link LastSeenMessages} state to a {@link CompletableFuture} that will + * provide the next-processed packet. This should include the fixed {@link LastSeenMessages}. + * @param timestamp the new {@link Instant} timestamp of this packet to update the internal chat state. + * @param lastSeenMessages the new {@link LastSeenMessages} last seen messages to update the internal chat state. */ - public void queuePacket(CompletableFuture nextPacket, Instant timestamp) { - synchronized (internalLock) { // wait for the lock to resolve - we don't want to drop packets - MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); - - CompletableFuture nextInLine = WrappedPacket.wrap(timestamp, nextPacket); - this.packetFuture = awaitChat(smc, this.packetFuture, - nextInLine); // we await chat, binding `this.packetFuture` -> `nextInLine` - } + public void queuePacket(Function> nextPacket, @Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { + queueTask((chatState, smc) -> { + LastSeenMessages newLastSeenMessages = chatState.updateFromMessage(timestamp, lastSeenMessages); + return nextPacket.apply(newLastSeenMessages).thenCompose(packet -> writePacket(packet, smc)); + }); } /** - * Hijacks the latest sent packet's timestamp to provide an in-order packet without polling the + * Hijacks the latest sent packet's chat state to provide an in-order packet without polling the * physical, or prior packets sent through the stream. * - * @param packet the {@link MinecraftPacket} to send. - * @param instantMapper the {@link InstantPacketMapper} which maps the prior timestamp and current - * packet to a new packet. - * @param the type of base to expect when mapping the packet. - * @param the type of packet for instantMapper type-checking. + * @param packetFunction a function that maps the prior {@link ChatState} into a new packet. + * @param the type of packet to send. */ - public void hijack(K packet, - InstantPacketMapper instantMapper) { - synchronized (internalLock) { - CompletableFuture trueFuture = CompletableFuture.completedFuture(packet); - MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); - - this.packetFuture = hijackCurrentPacket(smc, this.packetFuture, trueFuture, instantMapper); - } + public void queuePacket(Function packetFunction) { + queueTask((chatState, smc) -> { + T packet = packetFunction.apply(chatState); + return writePacket(packet, smc); + }); } - private static Function writePacket(MinecraftConnection connection) { - return wrappedPacket -> { - if (!connection.isClosed()) { - ChannelFuture future = wrappedPacket.write(connection); + public void handleAcknowledgement(int offset) { + queueTask((chatState, smc) -> { + int ackCountToForward = chatState.accumulateAckCount(offset); + if (ackCountToForward > 0) { + return writePacket(new ChatAcknowledgementPacket(ackCountToForward), smc); + } + return CompletableFuture.completedFuture(null); + }); + } + + private static CompletableFuture writePacket(T packet, MinecraftConnection smc) { + return CompletableFuture.runAsync(() -> { + if (!smc.isClosed()) { + ChannelFuture future = smc.write(packet); if (future != null) { future.awaitUninterruptibly(); } } - - return wrappedPacket; - }; + }, smc.eventLoop()); } - private static CompletableFuture awaitChat( - MinecraftConnection connection, - CompletableFuture binder, - CompletableFuture future - ) { - // the binder will run -> then the future will get the `write packet` caller - return binder.thenCompose(ignored -> future.thenApply(writePacket(connection))); - } - - private static CompletableFuture hijackCurrentPacket( - MinecraftConnection connection, - CompletableFuture binder, - CompletableFuture future, - InstantPacketMapper packetMapper - ) { - CompletableFuture awaitedFuture = new CompletableFuture<>(); - // the binder will complete -> then the future will get the `write packet` caller - binder.whenComplete((previous, ignored) -> { - // map the new packet into a better "designed" packet with the hijacked packet's timestamp - WrappedPacket.wrap(previous.timestamp, - future.thenApply(item -> packetMapper.map(previous.timestamp, item))) - .thenApplyAsync(writePacket(connection), connection.eventLoop()) - .whenComplete( - (packet, throwable) -> awaitedFuture.complete(throwable != null ? null : packet)); - }); - return awaitedFuture; + private interface Task { + CompletableFuture update(ChatState chatState, MinecraftConnection smc); } /** - * Provides an {@link Instant} based timestamp mapper from an existing object to create a packet. + * Tracks the last Secure Chat state that we received from the client. This is important to always have a valid 'last + * seen' state that is consistent with future and past updates from the client (which may be signed). This state is + * used to construct 'spoofed' command packets from the proxy to the server. + *

    + *
  • If we last forwarded a chat or command packet from the client, we have a known 'last seen' that we can + * reuse.
  • + *
  • If we last forwarded a {@link ChatAcknowledgementPacket}, the previous 'last seen' cannot be reused. We + * cannot predict an up-to-date 'last seen', as we do not know which messages the client actually saw.
  • + *
  • Therefore, we need to hold back any acknowledgement packets so that we can continue to reuse the last valid + * 'last seen' state.
  • + *
  • However, there is a limit to the number of messages that can remain unacknowledged on the server.
  • + *
  • To address this, we know that if the client has moved its 'last seen' window far enough, we can fill in the + * gap with dummy 'last seen', and it will never be checked.
  • + *
* - * @param The base object type to map. - * @param The resulting packet type. + * Note that this is effectively unused for 1.20.5+ clients, as commands without any signature do not send 'last seen' + * updates. */ - public interface InstantPacketMapper { + public static class ChatState { + private static final int MINIMUM_DELAYED_ACK_COUNT = LastSeenMessages.WINDOW_SIZE; + private static final BitSet DUMMY_LAST_SEEN_MESSAGES = new BitSet(); - /** - * Maps a value into a packet with it and a timestamp. - * - * @param nextInstant the {@link Instant} timestamp to use for tracking. - * @param currentObject the current item to map to the packet. - * @return The resulting packet from the mapping. - */ - V map(Instant nextInstant, K currentObject); - } + public volatile Instant lastTimestamp = Instant.EPOCH; + private volatile BitSet lastSeenMessages = new BitSet(); + private final AtomicInteger delayedAckCount = new AtomicInteger(); - private static class WrappedPacket { - - private final Instant timestamp; - private final MinecraftPacket packet; - - private WrappedPacket(Instant timestamp, MinecraftPacket packet) { - this.timestamp = timestamp; - this.packet = packet; + private ChatState() { } @Nullable - public ChannelFuture write(MinecraftConnection connection) { - if (packet != null) { - return connection.write(packet); + public LastSeenMessages updateFromMessage(@Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { + if (timestamp != null) { + this.lastTimestamp = timestamp; + } + if (lastSeenMessages != null) { + // We held back some acknowledged messages, so flush that out now that we have a known 'last seen' state again + int delayedAckCount = this.delayedAckCount.getAndSet(0); + this.lastSeenMessages = lastSeenMessages.getAcknowledged(); + return lastSeenMessages.offset(delayedAckCount); } return null; } - private static CompletableFuture wrap(Instant timestamp, - CompletableFuture nextPacket) { - return nextPacket - .thenApply(pkt -> new WrappedPacket(timestamp, pkt)) - .exceptionally(ignored -> new WrappedPacket(timestamp, null)); + public int accumulateAckCount(int ackCount) { + int delayedAckCount = this.delayedAckCount.addAndGet(ackCount); + int ackCountToForward = delayedAckCount - MINIMUM_DELAYED_ACK_COUNT; + if (ackCountToForward >= LastSeenMessages.WINDOW_SIZE) { + // Because we only forward acknowledgements above the window size, we don't have to shift the previous 'last seen' state + this.lastSeenMessages = DUMMY_LAST_SEEN_MESSAGES; + this.delayedAckCount.set(MINIMUM_DELAYED_ACK_COUNT); + return ackCountToForward; + } + return 0; + } + + public LastSeenMessages createLastSeen() { + return new LastSeenMessages(0, lastSeenMessages); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java index c778eecd0..9786fe145 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java @@ -23,11 +23,13 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import java.time.Instant; import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; import java.util.function.Function; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; public interface CommandHandler { @@ -53,11 +55,12 @@ public interface CommandHandler { } default void queueCommandResult(VelocityServer server, ConnectedPlayer player, - Function> futurePacketCreator, - String message, Instant timestamp) { - player.getChatQueue().queuePacket( - server.getCommandManager().callCommandEvent(player, message) - .thenComposeAsync(futurePacketCreator) + BiFunction> futurePacketCreator, + String message, Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { + CompletableFuture eventFuture = server.getCommandManager().callCommandEvent(player, message); + player.getChatQueue().queuePacket( + newLastSeenMessages -> eventFuture + .thenComposeAsync(event -> futurePacketCreator.apply(event, newLastSeenMessages)) .thenApply(pkt -> { if (server.getConfiguration().isLogCommandExecutions()) { logger.info("{} -> executed command /{}", player, message); @@ -68,6 +71,6 @@ public interface CommandHandler { player.sendMessage( Component.translatable("velocity.command.generic-error", NamedTextColor.RED)); return null; - }), timestamp); + }), timestamp, lastSeenMessages); } } 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 a1ed539d6..18a743c82 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 @@ -24,7 +24,8 @@ import java.util.BitSet; public class LastSeenMessages { - private static final int DIV_FLOOR = -Math.floorDiv(-20, 8); + public static final int WINDOW_SIZE = 20; + private static final int DIV_FLOOR = -Math.floorDiv(-WINDOW_SIZE, 8); private int offset; private BitSet acknowledged; @@ -33,6 +34,11 @@ public class LastSeenMessages { this.acknowledged = new BitSet(); } + public LastSeenMessages(int offset, BitSet acknowledged) { + this.offset = offset; + this.acknowledged = acknowledged; + } + public LastSeenMessages(ByteBuf buf) { this.offset = ProtocolUtils.readVarInt(buf); @@ -46,14 +52,18 @@ public class LastSeenMessages { buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR)); } - public boolean isEmpty() { - return acknowledged.isEmpty(); - } - public int getOffset() { return this.offset; } + public BitSet getAcknowledged() { + return acknowledged; + } + + public LastSeenMessages offset(final int offset) { + return new LastSeenMessages(this.offset + offset, acknowledged); + } + @Override public String toString() { return "LastSeenMessages{" + diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java index e9e24c2a0..9ac028646 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatType; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import java.time.Instant; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; @@ -36,6 +37,7 @@ public abstract class ChatBuilderV2 { protected @Nullable Identity senderIdentity; protected Instant timestamp; protected ChatType type = ChatType.CHAT; + protected @Nullable LastSeenMessages lastSeenMessages; protected ChatBuilderV2(ProtocolVersion version) { this.version = version; @@ -77,6 +79,11 @@ public abstract class ChatBuilderV2 { return this; } + public ChatBuilderV2 setLastSeenMessages(LastSeenMessages lastSeenMessages) { + this.lastSeenMessages = lastSeenMessages; + return this; + } + public abstract MinecraftPacket toClient(); public abstract MinecraftPacket toServer(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java index f4964e6c2..f8fc906ab 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java @@ -91,11 +91,12 @@ public class KeyedChatHandler implements }); } chatQueue.queuePacket( - chatFuture.exceptionally((ex) -> { + newLastSeen -> chatFuture.exceptionally((ex) -> { logger.error("Exception while handling player chat for {}", player, ex); return null; }), - packet.getExpiry() + packet.getExpiry(), + null ); } 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 bef75247c..1d3751e45 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 @@ -43,7 +43,7 @@ public class KeyedCommandHandler implements CommandHandler { + queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> { CommandExecuteEvent.CommandResult result = event.getResult(); IdentifiedKey playerKey = player.getIdentifiedKey(); if (result == CommandExecuteEvent.CommandResult.denied()) { @@ -111,6 +111,6 @@ public class KeyedCommandHandler implements CommandHandler { @Override public void handlePlayerCommandInternal(LegacyChatPacket packet) { String command = packet.getMessage().substring(1); - queueCommandResult(this.server, this.player, event -> { + queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> { CommandExecuteEvent.CommandResult result = event.getResult(); if (result == CommandExecuteEvent.CommandResult.denied()) { return CompletableFuture.completedFuture(null); @@ -62,6 +62,6 @@ public class LegacyCommandHandler implements CommandHandler { } return null; }); - }, command, Instant.now()); + }, command, Instant.now(), null); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java index 9a4fdc725..eb74123fc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java @@ -41,6 +41,7 @@ public class SessionChatBuilder extends ChatBuilderV2 { @Override public MinecraftPacket toServer() { + LastSeenMessages lastSeenMessages = this.lastSeenMessages != null ? this.lastSeenMessages : new LastSeenMessages(); if (message.startsWith("/")) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { UnsignedPlayerCommandPacket command = new UnsignedPlayerCommandPacket(); @@ -52,7 +53,7 @@ public class SessionChatBuilder extends ChatBuilderV2 { command.salt = 0L; command.timeStamp = timestamp; command.argumentSignatures = new SessionPlayerCommandPacket.ArgumentSignatures(); - command.lastSeenMessages = new LastSeenMessages(); + command.lastSeenMessages = lastSeenMessages; return command; } } else { @@ -62,8 +63,8 @@ public class SessionChatBuilder extends ChatBuilderV2 { chat.signature = new byte[0]; chat.timestamp = timestamp; chat.salt = 0L; - chat.lastSeenMessages = new LastSeenMessages(); + chat.lastSeenMessages = lastSeenMessages; return chat; } } -} \ No newline at end of file +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java index 20268f901..0731f64ed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java @@ -29,6 +29,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.concurrent.CompletableFuture; + public class SessionChatHandler implements ChatHandler { private static final Logger logger = LogManager.getLogger(SessionChatHandler.class); @@ -51,8 +53,9 @@ public class SessionChatHandler implements ChatHandler ChatQueue chatQueue = this.player.getChatQueue(); EventManager eventManager = this.server.getEventManager(); PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage()); + CompletableFuture eventFuture = eventManager.fire(toSend); chatQueue.queuePacket( - eventManager.fire(toSend) + newLastSeenMessages -> eventFuture .thenApply(pme -> { PlayerChatEvent.ChatResult chatResult = pme.getResult(); if (!chatResult.isAllowed()) { @@ -70,15 +73,17 @@ public class SessionChatHandler implements ChatHandler } return this.player.getChatBuilderFactory().builder().message(packet.message) .setTimestamp(packet.timestamp) + .setLastSeenMessages(newLastSeenMessages) .toServer(); } - return packet; + return packet.withLastSeenMessages(newLastSeenMessages); }) .exceptionally((ex) -> { logger.error("Exception while handling player chat for {}", player, ex); return null; }), - packet.getTimestamp() + packet.getTimestamp(), + packet.getLastSeenMessages() ); } } 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 6984970b3..0e47feede 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 @@ -18,13 +18,14 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.event.command.CommandExecuteEvent; -import com.velocitypowered.api.network.ProtocolVersion; 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 net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; public class SessionCommandHandler implements CommandHandler { @@ -41,78 +42,81 @@ public class SessionCommandHandler implements CommandHandler { + queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> { + SessionPlayerCommandPacket fixedPacket = packet.withLastSeenMessages(newLastSeenMessages); + CommandExecuteEvent.CommandResult result = event.getResult(); if (result == CommandExecuteEvent.CommandResult.denied()) { - if (packet.isSigned()) { - logger.fatal("A plugin tried to deny a command with signable component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); - player.disconnect(Component.text( - "A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - } - // We seemingly can't actually do this if signed args exist, if not, we can probs keep stuff happy - if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3) && packet.lastSeenMessages != null) { - return CompletableFuture.completedFuture(new ChatAcknowledgementPacket(packet.lastSeenMessages.getOffset())); - } - return CompletableFuture.completedFuture(null); + return CompletableFuture.completedFuture(consumeCommand(fixedPacket)); } - String commandToRun = result.getCommand().orElse(packet.command); + String commandToRun = result.getCommand().orElse(fixedPacket.command); if (result.isForwardToServer()) { - if (packet.isSigned() && commandToRun.equals(packet.command)) { - return CompletableFuture.completedFuture(packet); - } else { - if (packet.isSigned()) { - logger.fatal("A plugin tried to change a command with signed component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); - player.disconnect(Component.text( - "A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - return CompletableFuture.completedFuture(null); - } - - return CompletableFuture.completedFuture(this.player.getChatBuilderFactory() - .builder() - .setTimestamp(packet.timeStamp) - .asPlayer(this.player) - .message("/" + commandToRun) - .toServer()); - } + return CompletableFuture.completedFuture(forwardCommand(fixedPacket, commandToRun)); } return runCommand(this.server, this.player, commandToRun, hasRun -> { - if (!hasRun) { - if (packet.isSigned() && commandToRun.equals(packet.command)) { - return packet; - } else { - if (packet.isSigned()) { - logger.fatal("A plugin tried to change a command with signed component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); - player.disconnect(Component.text( - "A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - return null; - } - - return this.player.getChatBuilderFactory() - .builder() - .setTimestamp(packet.timeStamp) - .asPlayer(this.player) - .message("/" + commandToRun) - .toServer(); - } + if (hasRun) { + return consumeCommand(fixedPacket); } - if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3) && packet.lastSeenMessages != null) { - return new ChatAcknowledgementPacket(packet.lastSeenMessages.getOffset()); - } - return null; + return forwardCommand(fixedPacket, commandToRun); }); - }, packet.command, packet.timeStamp); + }, packet.command, packet.timeStamp, packet.lastSeenMessages); } } 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 03c47fec6..fc7ccf2c9 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 @@ -100,4 +100,15 @@ public class SessionPlayerChatPacket implements MinecraftPacket { buf.readBytes(signature); return signature; } + + public SessionPlayerChatPacket withLastSeenMessages(LastSeenMessages lastSeenMessages) { + SessionPlayerChatPacket packet = new SessionPlayerChatPacket(); + packet.message = message; + packet.timestamp = timestamp; + packet.salt = salt; + packet.signed = signed; + packet.signature = signature; + packet.lastSeenMessages = lastSeenMessages; + return packet; + } } 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 c3b1ae3c0..43ff8ed41 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 @@ -25,6 +25,8 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.time.Instant; import java.util.List; @@ -43,6 +45,8 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { this.salt = buf.readLong(); this.argumentSignatures = new ArgumentSignatures(buf); this.lastSeenMessages = new LastSeenMessages(buf); + + this.argumentSignatures = new ArgumentSignatures(); } @Override @@ -63,8 +67,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { } public boolean isSigned() { - if (salt == 0) return false; - return !lastSeenMessages.isEmpty() || !argumentSignatures.isEmpty(); + return !argumentSignatures.isEmpty(); } @Override @@ -83,6 +86,21 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { '}'; } + public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) { + if (lastSeenMessages == null) { + UnsignedPlayerCommandPacket packet = new UnsignedPlayerCommandPacket(); + packet.command = command; + return packet; + } + SessionPlayerCommandPacket packet = new SessionPlayerCommandPacket(); + packet.command = command; + packet.timeStamp = timeStamp; + packet.salt = salt; + packet.argumentSignatures = argumentSignatures; + packet.lastSeenMessages = lastSeenMessages; + return packet; + } + public static class ArgumentSignatures { private final List entries; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java index dd59e3ff2..b4e26fe07 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java @@ -19,7 +19,9 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket { @@ -33,6 +35,11 @@ public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket { ProtocolUtils.writeString(buf, this.command); } + @Override + public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) { + return this; + } + public boolean isSigned() { return false; } 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 bee080ee8..274bbb8f9 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 @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.protocol.packet.config; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; @@ -66,6 +67,13 @@ 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/server/ServerMap.java b/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java index d3fc5431f..267b78740 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java @@ -19,6 +19,8 @@ package com.velocitypowered.proxy.server; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.event.proxy.server.ServerRegisteredEvent; +import com.velocitypowered.api.event.proxy.server.ServerUnregisteredEvent; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.proxy.VelocityServer; @@ -84,6 +86,10 @@ public class ServerMap { throw new IllegalArgumentException( "Server with name " + serverInfo.getName() + " already registered"); } else if (existing == null) { + if (server != null) { + server.getEventManager().fireAndForget(new ServerRegisteredEvent(rs)); + } + return rs; } else { return existing; @@ -107,5 +113,9 @@ public class ServerMap { "Trying to remove server %s with differing information", serverInfo.getName()); Preconditions.checkState(servers.remove(lowerName, rs), "Server with name %s replaced whilst unregistering", serverInfo.getName()); + + if (server != null) { + server.getEventManager().fireAndForget(new ServerUnregisteredEvent(rs)); + } } } diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 3f4f6318c..e402305cc 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -1,8 +1,8 @@ # Config version. Do not change this config-version = "2.7" -# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25577. -bind = "0.0.0.0:25577" +# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565. +bind = "0.0.0.0:25565" # What should be the MOTD? This gets displayed when the player adds your server to # their server list. Only MiniMessage format is accepted. @@ -150,7 +150,7 @@ accepts-transfers = false enabled = false # If query is enabled, on what port should the query protocol listen on? -port = 25577 +port = 25565 # This is the map name that is reported to the query services. map = "Velocity"