13
0
geforkt von Mirrors/Velocity

Merge remote-tracking branch 'refs/remotes/upstream/dev/3.0.0'

Dieser Commit ist enthalten in:
Lixfel 2024-08-18 15:28:10 +02:00
Commit 6e33bc6c17
58 geänderte Dateien mit 1206 neuen und 380 gelöschten Zeilen

69
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normale Datei
Datei anzeigen

@ -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.
<details>
<summary>Example</summary>
```
[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
```
</details>
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).

10
.github/ISSUE_TEMPLATE/config.yml vendored Normale Datei
Datei anzeigen

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

48
.github/ISSUE_TEMPLATE/feature-request.yml vendored Normale Datei
Datei anzeigen

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

Datei anzeigen

@ -67,7 +67,9 @@ tasks {
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
"https://docs.oracle.com/en/java/javase/17/docs/api/", "https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/", "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( o.tags(

Datei anzeigen

@ -44,7 +44,7 @@ public interface CommandManager {
* @param otherAliases additional aliases * @param otherAliases additional aliases
* @throws IllegalArgumentException if one of the given aliases is already registered, or * @throws IllegalArgumentException if one of the given aliases is already registered, or
* the given command does not implement a registrable {@link Command} subinterface * 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) { default void register(String alias, Command command, String... otherAliases) {
register(metaBuilder(alias).aliases(otherAliases).build(), command); register(metaBuilder(alias).aliases(otherAliases).build(), command);
@ -65,7 +65,7 @@ public interface CommandManager {
* @param command the command to register * @param command the command to register
* @throws IllegalArgumentException if one of the given aliases is already registered, or * @throws IllegalArgumentException if one of the given aliases is already registered, or
* the given command does not implement a registrable {@link Command} subinterface * 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); void register(CommandMeta meta, Command command);

Datei anzeigen

@ -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<ResultedEvent.GenericResult> {
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");
}
}

Datei anzeigen

@ -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.
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
*
* @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) {
}

Datei anzeigen

@ -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 <b>not</b> called for the initial configuration of a player after login.
* <p>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.</p>
*
* @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) {
}

Datei anzeigen

@ -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.
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
* the {@linkplain Player#getProtocolState()} method is guaranteed
* to return {@link ProtocolState#CONFIGURATION}.</p>
*
* @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) {
}

Datei anzeigen

@ -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.
* <p>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}.</p>
*
* @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) {
}

Datei anzeigen

@ -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.
* <p>From this moment on, the {@link Player#getProtocolState()} method
* will return {@link ProtocolState#PLAY}.</p>
*
* @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) {
}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -87,7 +87,7 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_20_2(764, "1.20.2"), MINECRAFT_1_20_2(764, "1.20.2"),
MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"), MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"),
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), 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; private static final int SNAPSHOT_BIT = 30;

Datei anzeigen

@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.player.TabList;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.api.util.ServerLink;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -461,4 +462,16 @@ public interface Player extends
* @sinceMinecraft 1.20.5 * @sinceMinecraft 1.20.5
*/ */
void requestCookie(Key key); void requestCookie(Key key);
/**
* Send the player a list of custom links to display in their client's pause menu.
*
* <p>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<ServerLink> links);
} }

Datei anzeigen

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

Datei anzeigen

@ -11,7 +11,7 @@ shadow = "io.github.goooler.shadow:8.1.5"
spotless = "com.diffplug.spotless:6.25.0" spotless = "com.diffplug.spotless:6.25.0"
[libraries] [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" adventure-facet = "net.kyori:adventure-platform-facet:4.3.2"
asm = "org.ow2.asm:asm:9.6" asm = "org.ow2.asm:asm:9.6"
auto-service = "com.google.auto.service:auto-service:1.0.1" auto-service = "com.google.auto.service:auto-service:1.0.1"

Datei anzeigen

@ -45,7 +45,7 @@ import com.velocitypowered.proxy.command.builtin.ShutdownCommand;
import com.velocitypowered.proxy.command.builtin.VelocityCommand; import com.velocitypowered.proxy.command.builtin.VelocityCommand;
import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; 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.connection.util.ServerListPingHandler;
import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.console.VelocityConsole;
import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.crypto.EncryptionUtils;

Datei anzeigen

@ -379,7 +379,7 @@ public final class VelocityCommand {
this.heapGenerator.invoke(hotspotMbean, file.toString(), true); this.heapGenerator.invoke(hotspotMbean, file.toString(), true);
} catch (Throwable e1) { } catch (Throwable e1) {
// This should not occur // This should not occur
throw new RuntimeException(e); throw new RuntimeException(e1);
} }
src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN)); src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN));
}; };

Datei anzeigen

@ -60,7 +60,7 @@ public class VelocityConfiguration implements ProxyConfig {
private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class); private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class);
@Expose @Expose
private String bind = "0.0.0.0:25577"; private String bind = "0.0.0.0:25565";
@Expose @Expose
private String motd = "<aqua>A Velocity Server"; private String motd = "<aqua>A Velocity Server";
@Expose @Expose
@ -503,7 +503,7 @@ public class VelocityConfiguration implements ProxyConfig {
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED); 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 int maxPlayers = config.getIntOrElse("show-max-players", 500);
final boolean onlineMode = config.getOrElse("online-mode", true); final boolean onlineMode = config.getOrElse("online-mode", true);
final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true); final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
@ -830,7 +830,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose @Expose
private boolean queryEnabled = false; private boolean queryEnabled = false;
@Expose @Expose
private int queryPort = 25577; private int queryPort = 25565;
@Expose @Expose
private String queryMap = "Velocity"; private String queryMap = "Velocity";
@Expose @Expose
@ -849,7 +849,7 @@ public class VelocityConfiguration implements ProxyConfig {
private Query(CommentedConfig config) { private Query(CommentedConfig config) {
if (config != null) { if (config != null) {
this.queryEnabled = config.getOrElse("enabled", false); 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.queryMap = config.getOrElse("map", "Velocity");
this.showPlugins = config.getOrElse("show-plugins", false); this.showPlugins = config.getOrElse("show-plugins", false);
} }

Datei anzeigen

@ -47,7 +47,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; 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.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.util.except.QuietDecoderException; import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@ -148,13 +149,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
return; return;
} }
if (msg instanceof MinecraftPacket) { if (msg instanceof MinecraftPacket pkt) {
MinecraftPacket pkt = (MinecraftPacket) msg;
if (!pkt.handle(activeSessionHandler)) { if (!pkt.handle(activeSessionHandler)) {
activeSessionHandler.handleGeneric((MinecraftPacket) msg); activeSessionHandler.handleGeneric((MinecraftPacket) msg);
} }
} else if (msg instanceof HAProxyMessage) { } else if (msg instanceof HAProxyMessage proxyMessage) {
HAProxyMessage proxyMessage = (HAProxyMessage) msg;
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
proxyMessage.sourcePort()); proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) { } else if (msg instanceof ByteBuf) {
@ -383,9 +382,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (state == StateRegistry.CONFIG) { if (state == StateRegistry.CONFIG) {
// Activate the play packet queue // Activate the play packet queue
addPlayPacketQueueHandler(); addPlayPacketQueueHandler();
} else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) { } else {
// Remove the queue // 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. * Adds the play packet queue handler.
*/ */
public void addPlayPacketQueueHandler() { public void addPlayPacketQueueHandler() {
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueHandler(this.protocolVersion, new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
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()));
} }
} }

Datei anzeigen

@ -29,6 +29,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent;
import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.CookieStoreEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; 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.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@ -40,8 +41,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; 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.ResourcePackHandler; import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
@ -258,14 +259,26 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(RemoveResourcePackPacket packet) { public boolean handle(RemoveResourcePackPacket packet) {
final ConnectedPlayer player = serverConn.getPlayer(); final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent(
final ResourcePackHandler handler = player.resourcePackHandler(); packet.getId(), this.serverConn);
if (packet.getId() != null) { server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> {
handler.remove(packet.getId()); if (playerConnection.isClosed()) {
} else { return;
handler.clearAppliedResourcePacks(); }
} if (serverResourcePackRemoveEvent.getResult().isAllowed()) {
playerConnection.write(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);
}
}, playerConnection.eventLoop()).exceptionally((ex) -> {
logger.error("Exception while handling resource pack remove for {}", playerConnection, ex);
return null;
});
return true; return true;
} }

Datei anzeigen

@ -37,7 +37,6 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.Optional; import java.util.Optional;
import java.util.StringJoiner; import java.util.StringJoiner;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.ComponentSerializer; import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
@ -143,7 +142,7 @@ public class BungeeCordMessageResponder {
out.writeUTF("PlayerList"); out.writeUTF("PlayerList");
out.writeUTF(info.getServerInfo().getName()); out.writeUTF(info.getServerInfo().getName());
StringJoiner joiner = new StringJoiner(", "); final StringJoiner joiner = new StringJoiner(", ");
for (Player online : info.getPlayersConnected()) { for (Player online : info.getPlayersConnected()) {
joiner.add(online.getUsername()); joiner.add(online.getUsername());
} }
@ -187,10 +186,9 @@ public class BungeeCordMessageResponder {
Component messageComponent = serializer.deserialize(message); Component messageComponent = serializer.deserialize(message);
if (target.equals("ALL")) { if (target.equals("ALL")) {
proxy.sendMessage(Identity.nil(), messageComponent); proxy.sendMessage(messageComponent);
} else { } else {
proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(), proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent));
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) { private void processForwardToPlayer(ByteBufDataInput in) {
Optional<Player> player = proxy.getPlayer(in.readUTF()); Optional<Player> player = proxy.getPlayer(in.readUTF());
if (player.isPresent()) { if (player.isPresent()) {
@ -374,6 +379,9 @@ public class BungeeCordMessageResponder {
case "KickPlayer": case "KickPlayer":
this.processKick(in); this.processKick(in);
break; break;
case "KickPlayerRaw":
this.processKickRaw(in);
break;
default: default:
// Do nothing, unknown command // Do nothing, unknown command
break; break;

Datei anzeigen

@ -21,6 +21,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent;
import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.CookieStoreEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; 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.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; 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.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; 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.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; 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.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; 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.ResourcePackRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
@ -132,7 +135,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(KeepAlivePacket packet) { public boolean handle(KeepAlivePacket packet) {
serverConn.ensureConnected().write(packet); serverConn.getPendingPings().put(packet.getRandomId(), System.nanoTime());
serverConn.getPlayer().getConnection().write(packet);
return true; return true;
} }
@ -192,31 +196,53 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
} }
@Override @Override
public boolean handle(FinishedUpdatePacket packet) { public boolean handle(RemoveResourcePackPacket packet) {
MinecraftConnection smc = serverConn.ensureConnected(); final MinecraftConnection playerConnection = this.serverConn.getPlayer().getConnection();
ConnectedPlayer player = serverConn.getPlayer();
ClientConfigSessionHandler configHandler = final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent(
(ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); 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); 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()) { if (serverConn == player.getConnectedServer()) {
smc.setActiveSessionHandler(StateRegistry.PLAY); smc.setActiveSessionHandler(StateRegistry.PLAY);
player.sendPlayerListHeaderAndFooter( player.sendPlayerListHeaderAndFooter(player.getPlayerListHeader(), player.getPlayerListFooter());
player.getPlayerListHeader(), player.getPlayerListFooter());
// The client cleared the tab list. TODO: Restore changes done via TabList API // The client cleared the tab list. TODO: Restore changes done via TabList API
player.getTabList().clearAllSilent(); player.getTabList().clearAllSilent();
} else { } else {
smc.setActiveSessionHandler(StateRegistry.PLAY, smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture));
new TransitionSessionHandler(server, serverConn, resultFuture));
} }
if (player.resourcePackHandler().getFirstAppliedPack() == null if (player.resourcePackHandler().getFirstAppliedPack() == null && resourcePackToApply != null) {
&& resourcePackToApply != null) {
player.resourcePackHandler().queueResourcePack(resourcePackToApply); player.resourcePackHandler().queueResourcePack(resourcePackToApply);
} }
smc.setAutoReading(true);
}, smc.eventLoop()); }, smc.eventLoop());
return true; return true;
} }

Datei anzeigen

@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; 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.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
@ -142,10 +143,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(ServerLoginSuccessPacket packet) { public boolean handle(ServerLoginSuccessPacket packet) {
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) {
&& !informationForwarded) { resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer()));
resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE,
serverConn.getServer()));
serverConn.disconnect(); serverConn.disconnect();
return true; return true;
} }
@ -156,22 +155,20 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
// Move into the PLAY phase. // Move into the PLAY phase.
MinecraftConnection smc = serverConn.ensureConnected(); MinecraftConnection smc = serverConn.ensureConnected();
if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
smc.setActiveSessionHandler(StateRegistry.PLAY, smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture));
new TransitionSessionHandler(server, serverConn, resultFuture));
} else { } else {
smc.write(new LoginAcknowledgedPacket()); smc.write(new LoginAcknowledgedPacket());
smc.setActiveSessionHandler(StateRegistry.CONFIG, smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture));
new ConfigSessionHandler(server, serverConn, resultFuture));
ConnectedPlayer player = serverConn.getPlayer(); ConnectedPlayer player = serverConn.getPlayer();
if (player.getClientSettingsPacket() != null) { if (player.getClientSettingsPacket() != null) {
smc.write(player.getClientSettingsPacket()); smc.write(player.getClientSettingsPacket());
} }
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) { if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
smc.setAutoReading(false); smc.setAutoReading(false);
((ClientPlaySessionHandler) player.getConnection() clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
.getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> { } else {
smc.setAutoReading(true); // Initial login - the player is already in configuration state.
}, smc.eventLoop()); 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. The connection to the remote server was unexpectedly closed.
This is usually because the remote server does not have \ This is usually because the remote server does not have \
BungeeCord IP forwarding correctly enabled. 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.""")); on how to configure player info forwarding correctly."""));
} else { } else {
resultFuture.completeExceptionally( resultFuture.completeExceptionally(

Datei anzeigen

@ -178,14 +178,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data"));
} else { } else {
loginState = State.ACKNOWLEDGED; loginState = State.ACKNOWLEDGED;
mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, connectedPlayer));
new ClientConfigSessionHandler(server, connectedPlayer));
server.getEventManager().fire(new PostLoginEvent(connectedPlayer)) server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> {
.thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> { return connectToInitialServer(connectedPlayer);
logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); }).exceptionally((ex) -> {
return null; logger.error("Exception while connecting {} to initial server", connectedPlayer, ex);
}); return null;
});
} }
return true; return true;
} }
@ -224,8 +224,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
player.disconnect0(reason.get(), true); player.disconnect0(reason.get(), true);
} else { } else {
if (!server.registerConnection(player)) { if (!server.registerConnection(player)) {
player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), true);
true);
return; return;
} }
@ -238,13 +237,13 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
loginState = State.SUCCESS_SENT; loginState = State.SUCCESS_SENT;
if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
loginState = State.ACKNOWLEDGED; loginState = State.ACKNOWLEDGED;
mcConnection.setActiveSessionHandler(StateRegistry.PLAY, mcConnection.setActiveSessionHandler(StateRegistry.PLAY, new InitialConnectSessionHandler(player, server));
new InitialConnectSessionHandler(player, server)); server.getEventManager().fire(new PostLoginEvent(player)).thenCompose((ignored) -> {
server.getEventManager().fire(new PostLoginEvent(player)) return connectToInitialServer(player);
.thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> { }).exceptionally((ex) -> {
logger.error("Exception while connecting {} to initial server", player, ex); logger.error("Exception while connecting {} to initial server", player, ex);
return null; return null;
}); });
} }
} }
}, mcConnection.eventLoop()).exceptionally((ex) -> { }, mcConnection.eventLoop()).exceptionally((ex) -> {

Datei anzeigen

@ -19,6 +19,9 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; 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.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
@ -46,8 +49,6 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Handles the client config stage. * Handles the client config stage.
@ -59,6 +60,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
private final ConnectedPlayer player; private final ConnectedPlayer player;
private String brandChannel = null; private String brandChannel = null;
private CompletableFuture<?> configurationFuture;
private CompletableFuture<Void> configSwitchFuture; private CompletableFuture<Void> configSwitchFuture;
/** /**
@ -77,13 +79,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
configSwitchFuture = new CompletableFuture<>(); configSwitchFuture = new CompletableFuture<>();
} }
@Override
public void deactivated() {
configurationFuture = null;
}
@Override @Override
public boolean handle(final KeepAlivePacket packet) { public boolean handle(final KeepAlivePacket packet) {
final VelocityServerConnection serverConnection = player.getConnectedServer(); player.forwardKeepAlive(packet);
if (!this.sendKeepAliveToBackend(serverConnection, packet)) {
final VelocityServerConnection connectionInFlight = player.getConnectionInFlight();
this.sendKeepAliveToBackend(connectionInFlight, packet);
}
return true; return true;
} }
@ -104,8 +107,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(FinishedUpdatePacket packet) { public boolean handle(FinishedUpdatePacket packet) {
player.getConnection() player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player));
.setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player));
configSwitchFuture.complete(null); configSwitchFuture.complete(null);
return true; return true;
@ -139,12 +141,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(KnownPacksPacket packet) { public boolean handle(KnownPacksPacket packet) {
if (player.getConnectionInFlight() != null) { callConfigurationEvent().thenRun(() -> {
player.getConnectionInFlight().ensureConnected().write(packet); player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet);
return true; }).exceptionally(ex -> {
} logger.error("Error forwarding known packs response to backend:", ex);
return null;
});
return false; return true;
} }
@Override @Override
@ -207,26 +211,25 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void exception(Throwable throwable) { public void exception(Throwable throwable) {
player.disconnect( player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
} }
private boolean sendKeepAliveToBackend( /**
final @Nullable VelocityServerConnection serverConnection, * Calls the {@link PlayerConfigurationEvent}.
final @NotNull KeepAlivePacket packet * For 1.20.5+ backends this is done when the client responds to
) { * the known packs request. The response is delayed until the event
if (serverConnection != null) { * has been called.
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); * For 1.20.2-1.20.4 servers this is done when the client acknowledges
if (sentTime != null) { * the end of the configuration.
final MinecraftConnection smc = serverConnection.getConnection(); * This is handled differently because for 1.20.5+ servers can't keep
if (smc != null) { * their connection alive between states and older servers don't have
player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); * the known packs transaction.
smc.write(packet); */
return true; 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); 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().write(FinishedUpdatePacket.INSTANCE);
player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); 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; return configSwitchFuture;
} }
} }

Datei anzeigen

@ -27,6 +27,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent; 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.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; 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.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; 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.ChatHandler;
import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper; import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
@ -84,7 +86,6 @@ import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -176,17 +177,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(KeepAlivePacket packet) { public boolean handle(KeepAlivePacket packet) {
final VelocityServerConnection serverConnection = player.getConnectedServer(); player.forwardKeepAlive(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; return true;
} }
@ -406,6 +397,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Complete client switch // Complete client switch
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
VelocityServerConnection serverConnection = player.getConnectedServer(); VelocityServerConnection serverConnection = player.getConnectedServer();
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
if (serverConnection != null) { if (serverConnection != null) {
MinecraftConnection smc = serverConnection.ensureConnected(); MinecraftConnection smc = serverConnection.ensureConnected();
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
@ -421,6 +413,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
@Override
public boolean handle(ChatAcknowledgementPacket packet) {
if (player.getCurrentServer().isEmpty()) {
return true;
}
player.getChatQueue().handleAcknowledgement(packet.offset());
return true;
}
@Override @Override
public boolean handle(ServerboundCookieResponsePacket packet) { public boolean handle(ServerboundCookieResponsePacket packet) {
server.getEventManager() server.getEventManager()
@ -512,7 +513,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
* @return a future that completes when the switch is complete * @return a future that completes when the switch is complete
*/ */
public CompletableFuture<Void> doSwitch() { public CompletableFuture<Void> doSwitch() {
VelocityServerConnection existingConnection = player.getConnectedServer(); final VelocityServerConnection existingConnection = player.getConnectedServer();
if (existingConnection != null) { if (existingConnection != null) {
// Shut down the existing server connection. // Shut down the existing server connection.

Datei anzeigen

@ -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.PlayerModInfoEvent;
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent; 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.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.permission.PermissionFunction; 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.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.api.util.ServerLink;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation; import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
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.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; 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.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; 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.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; 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.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.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.config.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput;
@ -106,6 +112,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
@ -629,6 +636,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return connectionInFlight; return connectionInFlight;
} }
public VelocityServerConnection getConnectionInFlightOrConnectedServer() {
return connectionInFlight != null ? connectionInFlight : connectedServer;
}
public void resetInFlightConnection() { public void resetInFlightConnection() {
connectionInFlight = null; connectionInFlight = null;
} }
@ -806,7 +817,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}, connection.eventLoop()); }, connection.eventLoop());
} else if (event.getResult() instanceof final Notify res) { } else if (event.getResult() instanceof final Notify res) {
if (event.kickedDuringServerConnect() && previousConnection != null) { if (event.kickedDuringServerConnect() && previousConnection != null) {
sendMessage(Identity.nil(), res.getMessageComponent()); sendMessage(res.getMessageComponent());
} else { } else {
disconnect(res.getMessageComponent()); disconnect(res.getMessageComponent());
} }
@ -1057,6 +1068,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}, connection.eventLoop()); }, connection.eventLoop());
} }
@Override
public void setServerLinks(final @NotNull List<ServerLink> 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 @Override
public void addCustomChatCompletions(@NotNull Collection<String> completions) { public void addCustomChatCompletions(@NotNull Collection<String> completions) {
Preconditions.checkNotNull(completions, "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 "input cannot be greater than " + LegacyChatPacket.MAX_SERVERBOUND_MESSAGE_LENGTH
+ " characters in length"); + " characters in length");
if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) { if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
this.chatQueue.hijack(getChatBuilderFactory().builder().asPlayer(this).message(input), ChatBuilderV2 message = getChatBuilderFactory().builder().asPlayer(this).message(input);
(instant, item) -> { this.chatQueue.queuePacket(chatState -> {
item.setTimestamp(instant); message.setTimestamp(chatState.lastTimestamp);
return item.toServer(); message.setLastSeenMessages(chatState.createLastSeen());
}); return message.toServer();
});
} else { } else {
ensureBackendConnection().write(getChatBuilderFactory().builder() ensureBackendConnection().write(getChatBuilderFactory().builder()
.asPlayer(this).message(input).toServer()); .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. * Switches the connection to the client into config state.
*/ */
public void switchToConfigState() { public void switchToConfigState() {
CompletableFuture.runAsync(() -> { server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
connection.write(StartUpdatePacket.INSTANCE); .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
connection.getChannel().pipeline() if (bundleHandler.isInBundleSession()) {
.get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); bundleHandler.toggleBundleSession();
// Make sure we don't send any play packets to the player after update start connection.write(BundleDelimiterPacket.INSTANCE);
connection.addPlayPacketQueueHandler(); }
}, connection.eventLoop()).exceptionally((ex) -> { connection.write(StartUpdatePacket.INSTANCE);
logger.error("Error switching player connection to config state:", ex); connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
return null; // 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()) { switch (status.getStatus()) {
case ALREADY_CONNECTED: case ALREADY_CONNECTED -> sendMessage(ConnectionMessages.ALREADY_CONNECTED);
sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); case CONNECTION_IN_PROGRESS -> sendMessage(ConnectionMessages.IN_PROGRESS);
break; case CONNECTION_CANCELLED -> {
case CONNECTION_IN_PROGRESS:
sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS);
break;
case CONNECTION_CANCELLED:
// Ignored; the plugin probably already handled this. // Ignored; the plugin probably already handled this.
break; }
case SERVER_DISCONNECTED: case SERVER_DISCONNECTED -> {
Component reason = status.getReasonComponent() final Component reason = status.getReasonComponent()
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
handleConnectionException(toConnect, handleConnectionException(toConnect,
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe()); DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe());
break; }
default: default -> {
// The only remaining value is successful (no need to do anything!) // The only remaining value is successful (no need to do anything!)
break; }
} }
}, connection.eventLoop()).thenApply(Result::isSuccessful); }, connection.eventLoop()).thenApply(Result::isSuccessful);
} }

Datei anzeigen

@ -15,10 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.velocitypowered.proxy.connection.client; package com.velocitypowered.proxy.connection.player.bundle;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; 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.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;

Datei anzeigen

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.velocitypowered.proxy.connection.player; package com.velocitypowered.proxy.connection.player.resourcepack;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.ResourcePackInfo;

Datei anzeigen

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
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.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;

Datei anzeigen

@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
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.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;

Datei anzeigen

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
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.ListMultimap;
import com.google.common.collect.Multimaps; 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.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;

Datei anzeigen

@ -15,14 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
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.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; 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.ResourcePackRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;

Datei anzeigen

@ -35,7 +35,8 @@ public class Connections {
public static final String MINECRAFT_DECODER = "minecraft-decoder"; public static final String MINECRAFT_DECODER = "minecraft-decoder";
public static final String MINECRAFT_ENCODER = "minecraft-encoder"; public static final String MINECRAFT_ENCODER = "minecraft-encoder";
public static final String READ_TIMEOUT = "read-timeout"; 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() { private Connections() {
throw new AssertionError(); throw new AssertionError();

Datei anzeigen

@ -43,13 +43,13 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -86,35 +86,48 @@ public class VelocityPluginManager implements PluginManager {
checkNotNull(directory, "directory"); checkNotNull(directory, "directory");
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory"); checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
List<PluginDescription> found = new ArrayList<>(); Map<String, PluginDescription> foundCandidates = new LinkedHashMap<>();
JavaPluginLoader loader = new JavaPluginLoader(server, directory); JavaPluginLoader loader = new JavaPluginLoader(server, directory);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory,
p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
for (Path path : stream) { for (Path path : stream) {
try { 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("<UNKNOWN>"),
candidate.getId(),
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
}
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Unable to load plugin {}", path, e); logger.error("Unable to load plugin {}", path, e);
} }
} }
} }
if (found.isEmpty()) { if (foundCandidates.isEmpty()) {
// No plugins found // No plugins found
return; return;
} }
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found); List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
new ArrayList<>(foundCandidates.values()));
Set<String> loadedPluginsById = new HashSet<>(); Map<String, PluginDescription> loadedCandidates = new HashMap<>();
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>(); Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
// Now load the plugins // Now load the plugins
pluginLoad: pluginLoad:
for (PluginDescription candidate : sortedPlugins) { for (PluginDescription candidate : sortedPlugins) {
// Verify dependencies // Verify dependencies
for (PluginDependency dependency : candidate.getDependencies()) { 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(), logger.error("Can't load plugin {} due to missing dependency {}", candidate.getId(),
dependency.getId()); dependency.getId());
continue pluginLoad; continue pluginLoad;
@ -125,7 +138,7 @@ public class VelocityPluginManager implements PluginManager {
PluginDescription realPlugin = loader.createPluginFromCandidate(candidate); PluginDescription realPlugin = loader.createPluginFromCandidate(candidate);
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
pluginContainers.put(container, loader.createModule(container)); pluginContainers.put(container, loader.createModule(container));
loadedPluginsById.add(realPlugin.getId()); loadedCandidates.put(realPlugin.getId(), realPlugin);
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Can't create module for plugin {}", candidate.getId(), e); logger.error("Can't create module for plugin {}", candidate.getId(), e);
} }

Datei anzeigen

@ -56,8 +56,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
@Override @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) { if (msg instanceof ByteBuf buf) {
ByteBuf buf = (ByteBuf) msg;
tryDecode(ctx, buf); tryDecode(ctx, buf);
} else { } else {
ctx.fireChannelRead(msg); ctx.fireChannelRead(msg);
@ -147,4 +146,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
this.state = state; this.state = state;
this.setProtocolVersion(registry.version); this.setProtocolVersion(registry.version);
} }
public ProtocolUtils.Direction getDirection() {
return direction;
}
} }

Datei anzeigen

@ -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 <https://www.gnu.org/licenses/>.
*/
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.
*
* <p>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.
*
* <p>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<Object> queue = new ArrayDeque<>();
/**
* Provides registries for client &amp; 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);
}
}
}
}

Datei anzeigen

@ -25,7 +25,7 @@ import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.PlatformDependent; import java.util.ArrayDeque;
import java.util.Queue; import java.util.Queue;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -40,38 +40,36 @@ import org.jetbrains.annotations.NotNull;
* <p>This handler will queue up any packets that are sent to the client during this time, and send * <p>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. * 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 StateRegistry.PacketRegistry.ProtocolRegistry registry;
private final Queue<MinecraftPacket> queue = PlatformDependent.newMpscQueue(); private final Queue<MinecraftPacket> queue = new ArrayDeque<>();
/** /**
* Provides registries for client &amp; server bound packets. * Provides registries for client &amp; server bound packets.
* *
* @param version the protocol version * @param version the protocol version
*/ */
public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { public PlayPacketQueueOutboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) {
this.registry = this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version);
StateRegistry.CONFIG.getProtocolRegistry(direction, version);
} }
@Override @Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
throws Exception { if (!(msg instanceof final MinecraftPacket packet)) {
if (!(msg instanceof MinecraftPacket)) {
ctx.write(msg, promise); ctx.write(msg, promise);
return; return;
} }
// If the packet exists in the CONFIG state, we want to always // If the packet exists in the CONFIG state, we want to always
// ensure that it gets sent out to the client // ensure that it gets sent out to the client
if (this.registry.containsPacket(((MinecraftPacket) msg))) { if (this.registry.containsPacket(packet)) {
ctx.write(msg, promise); ctx.write(msg, promise);
return; return;
} }
// Otherwise, queue the packet // Otherwise, queue the packet
this.queue.offer((MinecraftPacket) msg); this.queue.offer(packet);
} }
@Override @Override
@ -87,10 +85,6 @@ public class PlayPacketQueueHandler extends ChannelDuplexHandler {
} }
private void releaseQueue(ChannelHandlerContext ctx, boolean active) { private void releaseQueue(ChannelHandlerContext ctx, boolean active) {
if (this.queue.isEmpty()) {
return;
}
// Send out all the queued packets // Send out all the queued packets
MinecraftPacket packet; MinecraftPacket packet;
while ((packet = this.queue.poll()) != null) { while ((packet = this.queue.poll()) != null) {

Datei anzeigen

@ -21,7 +21,7 @@ import com.google.common.base.Preconditions;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; 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.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;

Datei anzeigen

@ -54,4 +54,8 @@ public class ChatAcknowledgementPacket implements MinecraftPacket {
"offset=" + offset + "offset=" + offset +
'}'; '}';
} }
public int offset() {
return offset;
}
} }

Datei anzeigen

@ -23,7 +23,9 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import java.time.Instant; import java.time.Instant;
import java.util.BitSet;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; import java.util.function.Function;
/** /**
@ -32,9 +34,10 @@ import java.util.function.Function;
*/ */
public class ChatQueue { public class ChatQueue {
private final Object internalLock; private final Object internalLock = new Object();
private final ConnectedPlayer player; private final ConnectedPlayer player;
private CompletableFuture<WrappedPacket> packetFuture; private final ChatState chatState = new ChatState();
private CompletableFuture<Void> head = CompletableFuture.completedFuture(null);
/** /**
* Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}. * Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}.
@ -43,8 +46,19 @@ public class ChatQueue {
*/ */
public ChatQueue(ConnectedPlayer player) { public ChatQueue(ConnectedPlayer player) {
this.player = 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 * 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. * and messages. All entries are locked through an internal object lock.
* *
* @param nextPacket the {@link CompletableFuture} which will provide the next-processed packet. * @param nextPacket a function mapping {@link LastSeenMessages} state to a {@link CompletableFuture} that will
* @param timestamp the {@link Instant} timestamp of this packet so we can allow piggybacking. * 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<MinecraftPacket> nextPacket, Instant timestamp) { public void queuePacket(Function<LastSeenMessages, CompletableFuture<MinecraftPacket>> nextPacket, @Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) {
synchronized (internalLock) { // wait for the lock to resolve - we don't want to drop packets queueTask((chatState, smc) -> {
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); LastSeenMessages newLastSeenMessages = chatState.updateFromMessage(timestamp, lastSeenMessages);
return nextPacket.apply(newLastSeenMessages).thenCompose(packet -> writePacket(packet, smc));
CompletableFuture<WrappedPacket> nextInLine = WrappedPacket.wrap(timestamp, nextPacket); });
this.packetFuture = awaitChat(smc, this.packetFuture,
nextInLine); // we await chat, binding `this.packetFuture` -> `nextInLine`
}
} }
/** /**
* 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. * physical, or prior packets sent through the stream.
* *
* @param packet the {@link MinecraftPacket} to send. * @param packetFunction a function that maps the prior {@link ChatState} into a new packet.
* @param instantMapper the {@link InstantPacketMapper} which maps the prior timestamp and current * @param <T> the type of packet to send.
* packet to a new packet.
* @param <K> the type of base to expect when mapping the packet.
* @param <V> the type of packet for instantMapper type-checking.
*/ */
public <K, V extends MinecraftPacket> void hijack(K packet, public <T extends MinecraftPacket> void queuePacket(Function<ChatState, T> packetFunction) {
InstantPacketMapper<K, V> instantMapper) { queueTask((chatState, smc) -> {
synchronized (internalLock) { T packet = packetFunction.apply(chatState);
CompletableFuture<K> trueFuture = CompletableFuture.completedFuture(packet); return writePacket(packet, smc);
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); });
this.packetFuture = hijackCurrentPacket(smc, this.packetFuture, trueFuture, instantMapper);
}
} }
private static Function<WrappedPacket, WrappedPacket> writePacket(MinecraftConnection connection) { public void handleAcknowledgement(int offset) {
return wrappedPacket -> { queueTask((chatState, smc) -> {
if (!connection.isClosed()) { int ackCountToForward = chatState.accumulateAckCount(offset);
ChannelFuture future = wrappedPacket.write(connection); if (ackCountToForward > 0) {
return writePacket(new ChatAcknowledgementPacket(ackCountToForward), smc);
}
return CompletableFuture.completedFuture(null);
});
}
private static <T extends MinecraftPacket> CompletableFuture<Void> writePacket(T packet, MinecraftConnection smc) {
return CompletableFuture.runAsync(() -> {
if (!smc.isClosed()) {
ChannelFuture future = smc.write(packet);
if (future != null) { if (future != null) {
future.awaitUninterruptibly(); future.awaitUninterruptibly();
} }
} }
}, smc.eventLoop());
return wrappedPacket;
};
} }
private static <T extends MinecraftPacket> CompletableFuture<WrappedPacket> awaitChat( private interface Task {
MinecraftConnection connection, CompletableFuture<Void> update(ChatState chatState, MinecraftConnection smc);
CompletableFuture<WrappedPacket> binder,
CompletableFuture<WrappedPacket> future
) {
// the binder will run -> then the future will get the `write packet` caller
return binder.thenCompose(ignored -> future.thenApply(writePacket(connection)));
}
private static <K, V extends MinecraftPacket> CompletableFuture<WrappedPacket> hijackCurrentPacket(
MinecraftConnection connection,
CompletableFuture<WrappedPacket> binder,
CompletableFuture<K> future,
InstantPacketMapper<K, V> packetMapper
) {
CompletableFuture<WrappedPacket> 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;
} }
/** /**
* 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.
* <ul>
* <li>If we last forwarded a chat or command packet from the client, we have a known 'last seen' that we can
* reuse.</li>
* <li>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.</li>
* <li>Therefore, we need to hold back any acknowledgement packets so that we can continue to reuse the last valid
* 'last seen' state.</li>
* <li>However, there is a limit to the number of messages that can remain unacknowledged on the server.</li>
* <li>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.</li>
* </ul>
* *
* @param <K> The base object type to map. * Note that this is effectively unused for 1.20.5+ clients, as commands without any signature do not send 'last seen'
* @param <V> The resulting packet type. * updates.
*/ */
public interface InstantPacketMapper<K, V extends MinecraftPacket> { 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();
/** public volatile Instant lastTimestamp = Instant.EPOCH;
* Maps a value into a packet with it and a timestamp. private volatile BitSet lastSeenMessages = new BitSet();
* private final AtomicInteger delayedAckCount = new AtomicInteger();
* @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);
}
private static class WrappedPacket { private ChatState() {
private final Instant timestamp;
private final MinecraftPacket packet;
private WrappedPacket(Instant timestamp, MinecraftPacket packet) {
this.timestamp = timestamp;
this.packet = packet;
} }
@Nullable @Nullable
public ChannelFuture write(MinecraftConnection connection) { public LastSeenMessages updateFromMessage(@Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) {
if (packet != null) { if (timestamp != null) {
return connection.write(packet); 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; return null;
} }
private static CompletableFuture<WrappedPacket> wrap(Instant timestamp, public int accumulateAckCount(int ackCount) {
CompletableFuture<MinecraftPacket> nextPacket) { int delayedAckCount = this.delayedAckCount.addAndGet(ackCount);
return nextPacket int ackCountToForward = delayedAckCount - MINIMUM_DELAYED_ACK_COUNT;
.thenApply(pkt -> new WrappedPacket(timestamp, pkt)) if (ackCountToForward >= LastSeenMessages.WINDOW_SIZE) {
.exceptionally(ignored -> new WrappedPacket(timestamp, null)); // 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);
} }
} }
} }

Datei anzeigen

@ -23,11 +23,13 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import java.time.Instant; import java.time.Instant;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
public interface CommandHandler<T extends MinecraftPacket> { public interface CommandHandler<T extends MinecraftPacket> {
@ -53,11 +55,12 @@ public interface CommandHandler<T extends MinecraftPacket> {
} }
default void queueCommandResult(VelocityServer server, ConnectedPlayer player, default void queueCommandResult(VelocityServer server, ConnectedPlayer player,
Function<CommandExecuteEvent, CompletableFuture<MinecraftPacket>> futurePacketCreator, BiFunction<CommandExecuteEvent, LastSeenMessages, CompletableFuture<MinecraftPacket>> futurePacketCreator,
String message, Instant timestamp) { String message, Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) {
player.getChatQueue().queuePacket( CompletableFuture<CommandExecuteEvent> eventFuture = server.getCommandManager().callCommandEvent(player, message);
server.getCommandManager().callCommandEvent(player, message) player.getChatQueue().queuePacket(
.thenComposeAsync(futurePacketCreator) newLastSeenMessages -> eventFuture
.thenComposeAsync(event -> futurePacketCreator.apply(event, newLastSeenMessages))
.thenApply(pkt -> { .thenApply(pkt -> {
if (server.getConfiguration().isLogCommandExecutions()) { if (server.getConfiguration().isLogCommandExecutions()) {
logger.info("{} -> executed command /{}", player, message); logger.info("{} -> executed command /{}", player, message);
@ -68,6 +71,6 @@ public interface CommandHandler<T extends MinecraftPacket> {
player.sendMessage( player.sendMessage(
Component.translatable("velocity.command.generic-error", NamedTextColor.RED)); Component.translatable("velocity.command.generic-error", NamedTextColor.RED));
return null; return null;
}), timestamp); }), timestamp, lastSeenMessages);
} }
} }

Datei anzeigen

@ -24,7 +24,8 @@ import java.util.BitSet;
public class LastSeenMessages { 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 int offset;
private BitSet acknowledged; private BitSet acknowledged;
@ -33,6 +34,11 @@ public class LastSeenMessages {
this.acknowledged = new BitSet(); this.acknowledged = new BitSet();
} }
public LastSeenMessages(int offset, BitSet acknowledged) {
this.offset = offset;
this.acknowledged = acknowledged;
}
public LastSeenMessages(ByteBuf buf) { public LastSeenMessages(ByteBuf buf) {
this.offset = ProtocolUtils.readVarInt(buf); this.offset = ProtocolUtils.readVarInt(buf);
@ -46,14 +52,18 @@ public class LastSeenMessages {
buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR)); buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR));
} }
public boolean isEmpty() {
return acknowledged.isEmpty();
}
public int getOffset() { public int getOffset() {
return this.offset; return this.offset;
} }
public BitSet getAcknowledged() {
return acknowledged;
}
public LastSeenMessages offset(final int offset) {
return new LastSeenMessages(this.offset + offset, acknowledged);
}
@Override @Override
public String toString() { public String toString() {
return "LastSeenMessages{" + return "LastSeenMessages{" +

Datei anzeigen

@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatType; import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
import java.time.Instant; import java.time.Instant;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -36,6 +37,7 @@ public abstract class ChatBuilderV2 {
protected @Nullable Identity senderIdentity; protected @Nullable Identity senderIdentity;
protected Instant timestamp; protected Instant timestamp;
protected ChatType type = ChatType.CHAT; protected ChatType type = ChatType.CHAT;
protected @Nullable LastSeenMessages lastSeenMessages;
protected ChatBuilderV2(ProtocolVersion version) { protected ChatBuilderV2(ProtocolVersion version) {
this.version = version; this.version = version;
@ -77,6 +79,11 @@ public abstract class ChatBuilderV2 {
return this; return this;
} }
public ChatBuilderV2 setLastSeenMessages(LastSeenMessages lastSeenMessages) {
this.lastSeenMessages = lastSeenMessages;
return this;
}
public abstract MinecraftPacket toClient(); public abstract MinecraftPacket toClient();
public abstract MinecraftPacket toServer(); public abstract MinecraftPacket toServer();

Datei anzeigen

@ -91,11 +91,12 @@ public class KeyedChatHandler implements
}); });
} }
chatQueue.queuePacket( chatQueue.queuePacket(
chatFuture.exceptionally((ex) -> { newLastSeen -> chatFuture.exceptionally((ex) -> {
logger.error("Exception while handling player chat for {}", player, ex); logger.error("Exception while handling player chat for {}", player, ex);
return null; return null;
}), }),
packet.getExpiry() packet.getExpiry(),
null
); );
} }

Datei anzeigen

@ -43,7 +43,7 @@ public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPac
@Override @Override
public void handlePlayerCommandInternal(KeyedPlayerCommandPacket packet) { public void handlePlayerCommandInternal(KeyedPlayerCommandPacket packet) {
queueCommandResult(this.server, this.player, event -> { queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> {
CommandExecuteEvent.CommandResult result = event.getResult(); CommandExecuteEvent.CommandResult result = event.getResult();
IdentifiedKey playerKey = player.getIdentifiedKey(); IdentifiedKey playerKey = player.getIdentifiedKey();
if (result == CommandExecuteEvent.CommandResult.denied()) { if (result == CommandExecuteEvent.CommandResult.denied()) {
@ -111,6 +111,6 @@ public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPac
} }
return null; return null;
}); });
}, packet.getCommand(), packet.getTimestamp()); }, packet.getCommand(), packet.getTimestamp(), null);
} }
} }

Datei anzeigen

@ -42,7 +42,7 @@ public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
@Override @Override
public void handlePlayerCommandInternal(LegacyChatPacket packet) { public void handlePlayerCommandInternal(LegacyChatPacket packet) {
String command = packet.getMessage().substring(1); String command = packet.getMessage().substring(1);
queueCommandResult(this.server, this.player, event -> { queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> {
CommandExecuteEvent.CommandResult result = event.getResult(); CommandExecuteEvent.CommandResult result = event.getResult();
if (result == CommandExecuteEvent.CommandResult.denied()) { if (result == CommandExecuteEvent.CommandResult.denied()) {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
@ -62,6 +62,6 @@ public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
} }
return null; return null;
}); });
}, command, Instant.now()); }, command, Instant.now(), null);
} }
} }

Datei anzeigen

@ -41,6 +41,7 @@ public class SessionChatBuilder extends ChatBuilderV2 {
@Override @Override
public MinecraftPacket toServer() { public MinecraftPacket toServer() {
LastSeenMessages lastSeenMessages = this.lastSeenMessages != null ? this.lastSeenMessages : new LastSeenMessages();
if (message.startsWith("/")) { if (message.startsWith("/")) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
UnsignedPlayerCommandPacket command = new UnsignedPlayerCommandPacket(); UnsignedPlayerCommandPacket command = new UnsignedPlayerCommandPacket();
@ -52,7 +53,7 @@ public class SessionChatBuilder extends ChatBuilderV2 {
command.salt = 0L; command.salt = 0L;
command.timeStamp = timestamp; command.timeStamp = timestamp;
command.argumentSignatures = new SessionPlayerCommandPacket.ArgumentSignatures(); command.argumentSignatures = new SessionPlayerCommandPacket.ArgumentSignatures();
command.lastSeenMessages = new LastSeenMessages(); command.lastSeenMessages = lastSeenMessages;
return command; return command;
} }
} else { } else {
@ -62,7 +63,7 @@ public class SessionChatBuilder extends ChatBuilderV2 {
chat.signature = new byte[0]; chat.signature = new byte[0];
chat.timestamp = timestamp; chat.timestamp = timestamp;
chat.salt = 0L; chat.salt = 0L;
chat.lastSeenMessages = new LastSeenMessages(); chat.lastSeenMessages = lastSeenMessages;
return chat; return chat;
} }
} }

Datei anzeigen

@ -29,6 +29,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket> { public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket> {
private static final Logger logger = LogManager.getLogger(SessionChatHandler.class); private static final Logger logger = LogManager.getLogger(SessionChatHandler.class);
@ -51,8 +53,9 @@ public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket>
ChatQueue chatQueue = this.player.getChatQueue(); ChatQueue chatQueue = this.player.getChatQueue();
EventManager eventManager = this.server.getEventManager(); EventManager eventManager = this.server.getEventManager();
PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage()); PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage());
CompletableFuture<PlayerChatEvent> eventFuture = eventManager.fire(toSend);
chatQueue.queuePacket( chatQueue.queuePacket(
eventManager.fire(toSend) newLastSeenMessages -> eventFuture
.thenApply(pme -> { .thenApply(pme -> {
PlayerChatEvent.ChatResult chatResult = pme.getResult(); PlayerChatEvent.ChatResult chatResult = pme.getResult();
if (!chatResult.isAllowed()) { if (!chatResult.isAllowed()) {
@ -70,15 +73,17 @@ public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket>
} }
return this.player.getChatBuilderFactory().builder().message(packet.message) return this.player.getChatBuilderFactory().builder().message(packet.message)
.setTimestamp(packet.timestamp) .setTimestamp(packet.timestamp)
.setLastSeenMessages(newLastSeenMessages)
.toServer(); .toServer();
} }
return packet; return packet.withLastSeenMessages(newLastSeenMessages);
}) })
.exceptionally((ex) -> { .exceptionally((ex) -> {
logger.error("Exception while handling player chat for {}", player, ex); logger.error("Exception while handling player chat for {}", player, ex);
return null; return null;
}), }),
packet.getTimestamp() packet.getTimestamp(),
packet.getLastSeenMessages()
); );
} }
} }

Datei anzeigen

@ -18,13 +18,14 @@
package com.velocitypowered.proxy.protocol.packet.chat.session; package com.velocitypowered.proxy.protocol.packet.chat.session;
import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; 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.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> { public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> {
@ -41,78 +42,81 @@ public class SessionCommandHandler implements CommandHandler<SessionPlayerComman
return SessionPlayerCommandPacket.class; return SessionPlayerCommandPacket.class;
} }
@Nullable
private MinecraftPacket consumeCommand(SessionPlayerCommandPacket packet) {
if (packet.lastSeenMessages == null) {
return null;
}
if (packet.isSigned()) {
// Any signed message produced by the client *must* be passed through to the server in order to maintain a
// consistent state for future messages.
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."));
return null;
}
// An unsigned command with a 'last seen' update will not happen as of 1.20.5+, but for earlier versions - we still
// need to pass through the acknowledgement
final int offset = packet.lastSeenMessages.getOffset();
if (offset != 0) {
return new ChatAcknowledgementPacket(offset);
}
return null;
}
@Nullable
private MinecraftPacket forwardCommand(SessionPlayerCommandPacket packet, String newCommand) {
if (newCommand.equals(packet.command)) {
return packet;
}
return modifyCommand(packet, newCommand);
}
@Nullable
private MinecraftPacket modifyCommand(SessionPlayerCommandPacket packet, String newCommand) {
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)
.setLastSeenMessages(packet.lastSeenMessages)
.asPlayer(this.player)
.message("/" + newCommand)
.toServer();
}
@Override @Override
public void handlePlayerCommandInternal(SessionPlayerCommandPacket packet) { public void handlePlayerCommandInternal(SessionPlayerCommandPacket packet) {
queueCommandResult(this.server, this.player, event -> { queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> {
SessionPlayerCommandPacket fixedPacket = packet.withLastSeenMessages(newLastSeenMessages);
CommandExecuteEvent.CommandResult result = event.getResult(); CommandExecuteEvent.CommandResult result = event.getResult();
if (result == CommandExecuteEvent.CommandResult.denied()) { if (result == CommandExecuteEvent.CommandResult.denied()) {
if (packet.isSigned()) { return CompletableFuture.completedFuture(consumeCommand(fixedPacket));
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);
} }
String commandToRun = result.getCommand().orElse(packet.command); String commandToRun = result.getCommand().orElse(fixedPacket.command);
if (result.isForwardToServer()) { if (result.isForwardToServer()) {
if (packet.isSigned() && commandToRun.equals(packet.command)) { return CompletableFuture.completedFuture(forwardCommand(fixedPacket, commandToRun));
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 runCommand(this.server, this.player, commandToRun, hasRun -> { return runCommand(this.server, this.player, commandToRun, hasRun -> {
if (!hasRun) { if (hasRun) {
if (packet.isSigned() && commandToRun.equals(packet.command)) { return consumeCommand(fixedPacket);
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 (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3) && packet.lastSeenMessages != null) { return forwardCommand(fixedPacket, commandToRun);
return new ChatAcknowledgementPacket(packet.lastSeenMessages.getOffset());
}
return null;
}); });
}, packet.command, packet.timeStamp); }, packet.command, packet.timeStamp, packet.lastSeenMessages);
} }
} }

Datei anzeigen

@ -100,4 +100,15 @@ public class SessionPlayerChatPacket implements MinecraftPacket {
buf.readBytes(signature); buf.readBytes(signature);
return 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;
}
} }

Datei anzeigen

@ -25,6 +25,8 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
import com.velocitypowered.proxy.util.except.QuietDecoderException; import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
@ -63,8 +65,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket {
} }
public boolean isSigned() { public boolean isSigned() {
if (salt == 0) return false; return !argumentSignatures.isEmpty();
return !lastSeenMessages.isEmpty() || !argumentSignatures.isEmpty();
} }
@Override @Override
@ -83,6 +84,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 { public static class ArgumentSignatures {
private final List<ArgumentSignature> entries; private final List<ArgumentSignature> entries;

Datei anzeigen

@ -19,7 +19,9 @@ package com.velocitypowered.proxy.protocol.packet.chat.session;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket { public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket {
@ -33,6 +35,11 @@ public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket {
ProtocolUtils.writeString(buf, this.command); ProtocolUtils.writeString(buf, this.command);
} }
@Override
public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) {
return this;
}
public boolean isSigned() { public boolean isSigned() {
return false; return false;
} }

Datei anzeigen

@ -18,6 +18,7 @@
package com.velocitypowered.proxy.protocol.packet.config; package com.velocitypowered.proxy.protocol.packet.config;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.util.ServerLink;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; 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 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) { private static ServerLink read(ByteBuf buf, ProtocolVersion version) {
if (buf.readBoolean()) { if (buf.readBoolean()) {
return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf)); return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf));

Datei anzeigen

@ -19,6 +19,8 @@ package com.velocitypowered.proxy.server;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; 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.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
@ -84,6 +86,10 @@ public class ServerMap {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Server with name " + serverInfo.getName() + " already registered"); "Server with name " + serverInfo.getName() + " already registered");
} else if (existing == null) { } else if (existing == null) {
if (server != null) {
server.getEventManager().fireAndForget(new ServerRegisteredEvent(rs));
}
return rs; return rs;
} else { } else {
return existing; return existing;
@ -107,5 +113,9 @@ public class ServerMap {
"Trying to remove server %s with differing information", serverInfo.getName()); "Trying to remove server %s with differing information", serverInfo.getName());
Preconditions.checkState(servers.remove(lowerName, rs), Preconditions.checkState(servers.remove(lowerName, rs),
"Server with name %s replaced whilst unregistering", serverInfo.getName()); "Server with name %s replaced whilst unregistering", serverInfo.getName());
if (server != null) {
server.getEventManager().fireAndForget(new ServerUnregisteredEvent(rs));
}
} }
} }

Datei anzeigen

@ -1,8 +1,8 @@
# Config version. Do not change this # Config version. Do not change this
config-version = "2.7" config-version = "2.7"
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 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:25577" bind = "0.0.0.0:25565"
# What should be the MOTD? This gets displayed when the player adds your server to # What should be the MOTD? This gets displayed when the player adds your server to
# their server list. Only MiniMessage format is accepted. # their server list. Only MiniMessage format is accepted.
@ -150,7 +150,7 @@ accepts-transfers = false
enabled = false enabled = false
# If query is enabled, on what port should the query protocol listen on? # 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. # This is the map name that is reported to the query services.
map = "Velocity" map = "Velocity"