3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-11-17 05:20:14 +01:00

Update/chat order queue (#801)

* Implement chat queue for ordered chat.

* Update system to handle spoofed chat as well.

* Fix checkstyle erroring on bad indentation.

* Fix ChatQueue to use whenComplete instead of thenRun

* Merge upstream.

* Checkstyle

* Deny denied commands.
Dieser Commit ist enthalten in:
Corey Shupe 2022-08-02 20:50:01 -04:00 committet von GitHub
Ursprung 1a3fba4250
Commit e5b84ecf1d
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
4 geänderte Dateien mit 291 neuen und 98 gelöschten Zeilen

Datei anzeigen

@ -58,6 +58,7 @@ import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer;
import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder; import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder;
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand;
@ -139,10 +140,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
private void processCommandMessage(String message, @Nullable SignedChatCommand signedCommand, private void processCommandMessage(String message, @Nullable SignedChatCommand signedCommand,
MinecraftPacket original) { MinecraftPacket original, Instant passedTimestamp) {
server.getCommandManager().callCommandEvent(player, message) this.player.getChatQueue().queuePacket(server.getCommandManager().callCommandEvent(player, message)
.thenComposeAsync(event -> processCommandExecuteResult(message, .thenComposeAsync(event -> processCommandExecuteResult(message,
event.getResult(), signedCommand)) event.getResult(), signedCommand, passedTimestamp))
.whenComplete((ignored, throwable) -> { .whenComplete((ignored, throwable) -> {
if (server.getConfiguration().isLogCommandExecutions()) { if (server.getConfiguration().isLogCommandExecutions()) {
logger.info("{} -> executed command /{}", player, message); logger.info("{} -> executed command /{}", player, message);
@ -154,7 +155,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.sendMessage(Component.translatable("velocity.command.generic-error", player.sendMessage(Component.translatable("velocity.command.generic-error",
NamedTextColor.RED)); NamedTextColor.RED));
return null; return null;
}); }), passedTimestamp);
} }
private void processPlayerChat(String message, @Nullable SignedChatMessage signedMessage, private void processPlayerChat(String message, @Nullable SignedChatMessage signedMessage,
@ -163,48 +164,61 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (smc == null) { if (smc == null) {
return; return;
} }
PlayerChatEvent event = new PlayerChatEvent(player, message);
server.getEventManager().fire(event) if (signedMessage == null) {
.thenAcceptAsync(pme -> { PlayerChatEvent event = new PlayerChatEvent(player, message);
callChat(original, event, null).thenAccept(smc::write);
} else {
Instant messageTimestamp = signedMessage.getExpiryTemporal();
PlayerChatEvent event = new PlayerChatEvent(player, message);
this.player.getChatQueue().queuePacket(callChat(original, event, signedMessage), messageTimestamp);
}
}
private CompletableFuture<MinecraftPacket> callChat(MinecraftPacket original, PlayerChatEvent event,
@Nullable SignedChatMessage signedMessage) {
return server.getEventManager().fire(event)
.thenApply(pme -> {
PlayerChatEvent.ChatResult chatResult = pme.getResult(); PlayerChatEvent.ChatResult chatResult = pme.getResult();
if (chatResult.isAllowed()) { if (chatResult.isAllowed()) {
Optional<String> eventMsg = pme.getResult().getMessage(); Optional<String> eventMsg = pme.getResult().getMessage();
if (eventMsg.isPresent()) { if (eventMsg.isPresent()) {
String messageNew = eventMsg.get(); String messageNew = eventMsg.get();
if (player.getIdentifiedKey() != null) { if (player.getIdentifiedKey() != null) {
if (!messageNew.equals(signedMessage.getMessage())) { if (signedMessage != null && !messageNew.equals(signedMessage.getMessage())) {
if (player.getIdentifiedKey().getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { if (player.getIdentifiedKey().getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) {
// Bad, very bad. // Bad, very bad.
logger.fatal("A plugin tried to change a signed chat message. " logger.fatal("A plugin tried to change a signed chat message. "
+ "This is no longer possible in 1.19.1 and newer. " + "This is no longer possible in 1.19.1 and newer. "
+ "Disconnecting player " + player.getUsername()); + "Disconnecting player " + player.getUsername());
player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. "
+ "Contact your network administrator.")); + "Contact your network administrator."));
} else { } else {
logger.warn("A plugin changed a signed chat message. The server may not accept it."); logger.warn("A plugin changed a signed chat message. The server may not accept it.");
smc.write(ChatBuilder.builder(player.getProtocolVersion()) return ChatBuilder.builder(player.getProtocolVersion())
.message(messageNew).toServer()); .message(messageNew).toServer();
} }
} else { } else {
smc.write(original); return original;
} }
} else { } else {
smc.write(ChatBuilder.builder(player.getProtocolVersion()) return ChatBuilder.builder(player.getProtocolVersion())
.message(messageNew).toServer()); .message(messageNew).toServer();
} }
} else { } else {
smc.write(original); return original;
} }
} else { } else {
if (player.getIdentifiedKey().getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { if (player.getIdentifiedKey().getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) {
logger.fatal("A plugin tried to cancel a signed chat message." logger.fatal("A plugin tried to cancel a signed chat message."
+ " This is no longer possible in 1.19.1 and newer. " + " This is no longer possible in 1.19.1 and newer. "
+ "Disconnecting player " + player.getUsername()); + "Disconnecting player " + player.getUsername());
player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. "
+ "Contact your network administrator.")); + "Contact your network administrator."));
} }
} }
}, smc.eventLoop()) return null;
})
.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;
@ -260,12 +274,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (!packet.isUnsigned()) { if (!packet.isUnsigned()) {
SignedChatCommand signedCommand = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false); SignedChatCommand signedCommand = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false);
if (signedCommand != null) { if (signedCommand != null) {
processCommandMessage(packet.getCommand(), signedCommand, packet); processCommandMessage(packet.getCommand(), signedCommand, packet, packet.getTimestamp());
return true; return true;
} }
} }
processCommandMessage(packet.getCommand(), null, packet); processCommandMessage(packet.getCommand(), null, packet, packet.getTimestamp());
return true; return true;
} }
@ -303,7 +317,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
if (msg.startsWith("/")) { if (msg.startsWith("/")) {
processCommandMessage(msg.substring(1), null, packet); processCommandMessage(msg.substring(1), null, packet, Instant.now());
} else { } else {
processPlayerChat(msg, null, packet); processPlayerChat(msg, null, packet);
} }
@ -723,77 +737,72 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
private CompletableFuture<Void> processCommandExecuteResult(String originalCommand, private CompletableFuture<MinecraftPacket> processCommandExecuteResult(String originalCommand,
CommandResult result, CommandResult result,
@Nullable SignedChatCommand signedCommand) { @Nullable SignedChatCommand signedCommand,
Instant passedTimestamp) {
IdentifiedKey playerKey = player.getIdentifiedKey(); IdentifiedKey playerKey = player.getIdentifiedKey();
if (result == CommandResult.denied() && playerKey != null) { if (result == CommandResult.denied() && playerKey != null) {
if (signedCommand != null && playerKey.getKeyRevision() if (signedCommand != null && playerKey.getKeyRevision()
.compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { .compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) {
logger.fatal("A plugin tried to deny a command with signable component(s). " logger.fatal("A plugin tried to deny a command with signable component(s). "
+ "This is not supported. " + "This is not supported. "
+ "Disconnecting player " + player.getUsername()); + "Disconnecting player " + player.getUsername());
player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. "
+ "Contact your network administrator.")); + "Contact your network administrator."));
} }
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
} }
MinecraftConnection smc;
try {
smc = player.ensureAndGetCurrentServer().ensureConnected();
} catch (Exception ex) {
player.disconnect(Component.translatable("velocity.error.player-connection-error",
NamedTextColor.RED));
return CompletableFuture.completedFuture(null);
}
String commandToRun = result.getCommand().orElse(originalCommand); String commandToRun = result.getCommand().orElse(originalCommand);
if (result.isForwardToServer()) { if (result.isForwardToServer()) {
ChatBuilder write = ChatBuilder ChatBuilder write = ChatBuilder
.builder(player.getProtocolVersion()) .builder(player.getProtocolVersion())
.timestamp(passedTimestamp)
.asPlayer(player); .asPlayer(player);
if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) { if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) {
write.message(signedCommand); write.message(signedCommand);
} else { } else {
if (signedCommand != null && playerKey != null && playerKey.getKeyRevision() if (signedCommand != null && playerKey != null && playerKey.getKeyRevision()
.compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { .compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) {
logger.fatal("A plugin tried to change a command with signed component(s). " logger.fatal("A plugin tried to change a command with signed component(s). "
+ "This is not supported. " + "This is not supported. "
+ "Disconnecting player " + player.getUsername()); + "Disconnecting player " + player.getUsername());
player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. "
+ "Contact your network administrator.")); + "Contact your network administrator."));
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
} }
write.message("/" + commandToRun); write.message("/" + commandToRun);
} }
return CompletableFuture.runAsync(() -> smc.write(write.toServer()), smc.eventLoop()); return CompletableFuture.completedFuture(write.toServer());
} else { } else {
return server.getCommandManager().executeImmediatelyAsync(player, commandToRun) return server.getCommandManager().executeImmediatelyAsync(player, commandToRun)
.thenAcceptAsync(hasRun -> { .thenApply(hasRun -> {
if (!hasRun) { if (!hasRun) {
ChatBuilder write = ChatBuilder ChatBuilder write = ChatBuilder
.builder(player.getProtocolVersion()) .builder(player.getProtocolVersion())
.timestamp(passedTimestamp)
.asPlayer(player); .asPlayer(player);
if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) { if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) {
write.message(signedCommand); write.message(signedCommand);
} else { } else {
if (signedCommand != null && playerKey != null && playerKey.getKeyRevision() if (signedCommand != null && playerKey != null && playerKey.getKeyRevision()
.compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { .compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) {
logger.fatal("A plugin tried to change a command with signed component(s). " logger.fatal("A plugin tried to change a command with signed component(s). "
+ "This is not supported. " + "This is not supported. "
+ "Disconnecting player " + player.getUsername()); + "Disconnecting player " + player.getUsername());
player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. " player.disconnect(Component.text("A proxy plugin caused an illegal protocol state. "
+ "Contact your network administrator.")); + "Contact your network administrator."));
return; return null;
} }
write.message("/" + commandToRun); write.message("/" + commandToRun);
} }
smc.write(write.toServer()); return write.toServer();
} }
}, smc.eventLoop()); return null;
});
} }
} }

Datei anzeigen

@ -66,6 +66,7 @@ import com.velocitypowered.proxy.protocol.packet.KeepAlive;
import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest;
import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder; import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder;
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat;
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.server.VelocityRegisteredServer;
@ -155,19 +156,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private @Nullable ResourcePackInfo pendingResourcePack; private @Nullable ResourcePackInfo pendingResourcePack;
private @Nullable ResourcePackInfo appliedResourcePack; private @Nullable ResourcePackInfo appliedResourcePack;
private final @NotNull Pointers pointers = Player.super.pointers().toBuilder() private final @NotNull Pointers pointers = Player.super.pointers().toBuilder()
.withDynamic(Identity.UUID, this::getUniqueId) .withDynamic(Identity.UUID, this::getUniqueId)
.withDynamic(Identity.NAME, this::getUsername) .withDynamic(Identity.NAME, this::getUsername)
.withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername()))
.withDynamic(Identity.LOCALE, this::getEffectiveLocale) .withDynamic(Identity.LOCALE, this::getEffectiveLocale)
.withStatic(PermissionChecker.POINTER, getPermissionChecker()) .withStatic(PermissionChecker.POINTER, getPermissionChecker())
.withStatic(FacetPointers.TYPE, Type.PLAYER) .withStatic(FacetPointers.TYPE, Type.PLAYER)
.build(); .build();
private @Nullable String clientBrand; private @Nullable String clientBrand;
private @Nullable Locale effectiveLocale; private @Nullable Locale effectiveLocale;
private @Nullable IdentifiedKey playerKey; private @Nullable IdentifiedKey playerKey;
private ChatQueue chatQueue;
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
@Nullable InetSocketAddress virtualHost, boolean onlineMode, @Nullable IdentifiedKey playerKey) { @Nullable InetSocketAddress virtualHost, boolean onlineMode, @Nullable IdentifiedKey playerKey) {
this.server = server; this.server = server;
this.profile = profile; this.profile = profile;
this.connection = connection; this.connection = connection;
@ -183,6 +185,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.tabList = new VelocityTabListLegacy(this, server); this.tabList = new VelocityTabListLegacy(this, server);
} }
this.playerKey = playerKey; this.playerKey = playerKey;
this.chatQueue = new ChatQueue(this);
}
ChatQueue getChatQueue() {
return chatQueue;
} }
@Override @Override
@ -220,6 +227,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
/** /**
* Makes sure the player is connected to a server and returns the server they are connected to. * Makes sure the player is connected to a server and returns the server they are connected to.
*
* @return the server the player is connected to * @return the server the player is connected to
*/ */
public VelocityServerConnection ensureAndGetCurrentServer() { public VelocityServerConnection ensureAndGetCurrentServer() {
@ -320,21 +328,21 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component translated = translateMessage(message); Component translated = translateMessage(message);
connection.write(ChatBuilder.builder(this.getProtocolVersion()) connection.write(ChatBuilder.builder(this.getProtocolVersion())
.component(translated).forIdentity(identity).toClient()); .component(translated).forIdentity(identity).toClient());
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message, public void sendMessage(@NonNull Identity identity, @NonNull Component message,
@NonNull MessageType type) { @NonNull MessageType type) {
Preconditions.checkNotNull(message, "message"); Preconditions.checkNotNull(message, "message");
Preconditions.checkNotNull(type, "type"); Preconditions.checkNotNull(type, "type");
Component translated = translateMessage(message); Component translated = translateMessage(message);
connection.write(ChatBuilder.builder(this.getProtocolVersion()) connection.write(ChatBuilder.builder(this.getProtocolVersion())
.component(translated).forIdentity(identity) .component(translated).forIdentity(identity)
.setType(type == MessageType.CHAT ? ChatBuilder.ChatType.CHAT : ChatBuilder.ChatType.SYSTEM) .setType(type == MessageType.CHAT ? ChatBuilder.ChatType.CHAT : ChatBuilder.ChatType.SYSTEM)
.toClient()); .toClient());
} }
@Override @Override
@ -345,7 +353,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
if (playerVersion.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0) { if (playerVersion.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0) {
// Use the title packet instead. // Use the title packet instead.
GenericTitlePacket pkt = GenericTitlePacket.constructTitlePacket( GenericTitlePacket pkt = GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.SET_ACTION_BAR, playerVersion); GenericTitlePacket.ActionType.SET_ACTION_BAR, playerVersion);
pkt.setComponent(ProtocolUtils.getJsonChatSerializer(playerVersion) pkt.setComponent(ProtocolUtils.getJsonChatSerializer(playerVersion)
.serialize(translated)); .serialize(translated));
connection.write(pkt); connection.write(pkt);
@ -465,7 +473,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
public void clearTitle() { public void clearTitle() {
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
connection.write(GenericTitlePacket.constructTitlePacket( connection.write(GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.HIDE, this.getProtocolVersion())); GenericTitlePacket.ActionType.HIDE, this.getProtocolVersion()));
} }
} }
@ -473,7 +481,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
public void resetTitle() { public void resetTitle() {
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
connection.write(GenericTitlePacket.constructTitlePacket( connection.write(GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.RESET, this.getProtocolVersion())); GenericTitlePacket.ActionType.RESET, this.getProtocolVersion()));
} }
} }
@ -497,7 +505,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
private ConnectionRequestBuilder createConnectionRequest(RegisteredServer server, private ConnectionRequestBuilder createConnectionRequest(RegisteredServer server,
@Nullable VelocityServerConnection previousConnection) { @Nullable VelocityServerConnection previousConnection) {
return new ConnectionRequestBuilderImpl(server, previousConnection); return new ConnectionRequestBuilderImpl(server, previousConnection);
} }
@ -532,7 +540,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
/** /**
* Disconnects the player from the proxy. * Disconnects the player from the proxy.
* @param reason the reason for disconnecting the player *
* @param reason the reason for disconnecting the player
* @param duringLogin whether the disconnect happened during login * @param duringLogin whether the disconnect happened during login
*/ */
public void disconnect0(Component reason, boolean duringLogin) { public void disconnect0(Component reason, boolean duringLogin) {
@ -559,12 +568,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
/** /**
* Handles unexpected disconnects. * Handles unexpected disconnects.
* @param server the server we disconnected from *
* @param server the server we disconnected from
* @param throwable the exception * @param throwable the exception
* @param safe whether or not we can safely reconnect to a new server * @param safe whether or not we can safely reconnect to a new server
*/ */
public void handleConnectionException(RegisteredServer server, Throwable throwable, public void handleConnectionException(RegisteredServer server, Throwable throwable,
boolean safe) { boolean safe) {
if (!isActive()) { if (!isActive()) {
// If the connection is no longer active, it makes no sense to try and recover it. // If the connection is no longer active, it makes no sense to try and recover it.
return; return;
@ -597,12 +607,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
/** /**
* Handles unexpected disconnects. * Handles unexpected disconnects.
* @param server the server we disconnected from *
* @param server the server we disconnected from
* @param disconnect the disconnect packet * @param disconnect the disconnect packet
* @param safe whether or not we can safely reconnect to a new server * @param safe whether or not we can safely reconnect to a new server
*/ */
public void handleConnectionException(RegisteredServer server, Disconnect disconnect, public void handleConnectionException(RegisteredServer server, Disconnect disconnect,
boolean safe) { boolean safe) {
if (!isActive()) { if (!isActive()) {
// If the connection is no longer active, it makes no sense to try and recover it. // If the connection is no longer active, it makes no sense to try and recover it.
return; return;
@ -628,7 +639,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
private void handleConnectionException(RegisteredServer rs, private void handleConnectionException(RegisteredServer rs,
@Nullable Component kickReason, Component friendlyReason, boolean safe) { @Nullable Component kickReason, Component friendlyReason, boolean safe) {
if (!isActive()) { if (!isActive()) {
// If the connection is no longer active, it makes no sense to try and recover it. // If the connection is no longer active, it makes no sense to try and recover it.
return; return;
@ -661,7 +672,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason, private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason,
boolean kickedFromCurrent) { boolean kickedFromCurrent) {
server.getEventManager().fire(originalEvent) server.getEventManager().fire(originalEvent)
.thenAcceptAsync(event -> { .thenAcceptAsync(event -> {
// There can't be any connection in flight now. // There can't be any connection in flight now.
@ -696,7 +707,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
// Impossible/nonsensical cases // Impossible/nonsensical cases
case ALREADY_CONNECTED: case ALREADY_CONNECTED:
case CONNECTION_IN_PROGRESS: case CONNECTION_IN_PROGRESS:
// Fatal case // Fatal case
case CONNECTION_CANCELLED: case CONNECTION_CANCELLED:
Component fallbackMsg = res.getMessageComponent(); Component fallbackMsg = res.getMessageComponent();
if (fallbackMsg == null) { if (fallbackMsg == null) {
@ -753,7 +764,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
* server. * server.
* *
* @param current the "current" server that the player is on, useful as an override * @param current the "current" server that the player is on, useful as an override
*
* @return the next server to try * @return the next server to try
*/ */
private Optional<RegisteredServer> getNextServerToTry(@Nullable RegisteredServer current) { private Optional<RegisteredServer> getNextServerToTry(@Nullable RegisteredServer current) {
@ -897,8 +907,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Preconditions.checkArgument(input.length() <= LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH, Preconditions.checkArgument(input.length() <= LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH,
"input cannot be greater than " + LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH "input cannot be greater than " + LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH
+ " characters in length"); + " characters in length");
ensureBackendConnection().write(ChatBuilder.builder(getProtocolVersion()) if (getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
.asPlayer(this).message(input).toServer()); this.chatQueue.hijack(ChatBuilder.builder(getProtocolVersion()).asPlayer(this).message(input),
(instant, item) -> {
item.timestamp(instant);
return item.toServer();
});
} else {
ensureBackendConnection().write(ChatBuilder.builder(getProtocolVersion())
.asPlayer(this).message(input).toServer());
}
} }
@Override @Override
@ -943,7 +961,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
while (!outstandingResourcePacks.isEmpty()) { while (!outstandingResourcePacks.isEmpty()) {
queued = outstandingResourcePacks.peek(); queued = outstandingResourcePacks.peek();
if (queued.getShouldForce() && getProtocolVersion() if (queued.getShouldForce() && getProtocolVersion()
.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { .compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
break; break;
} }
onResourcePackResponse(PlayerResourcePackStatusEvent.Status.DECLINED); onResourcePackResponse(PlayerResourcePackStatusEvent.Status.DECLINED);
@ -985,19 +1003,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
public boolean onResourcePackResponse(PlayerResourcePackStatusEvent.Status status) { public boolean onResourcePackResponse(PlayerResourcePackStatusEvent.Status status) {
final boolean peek = status == PlayerResourcePackStatusEvent.Status.ACCEPTED; final boolean peek = status == PlayerResourcePackStatusEvent.Status.ACCEPTED;
final ResourcePackInfo queued = peek final ResourcePackInfo queued = peek
? outstandingResourcePacks.peek() : outstandingResourcePacks.poll(); ? outstandingResourcePacks.peek() : outstandingResourcePacks.poll();
server.getEventManager().fire(new PlayerResourcePackStatusEvent(this, status, queued)) server.getEventManager().fire(new PlayerResourcePackStatusEvent(this, status, queued))
.thenAcceptAsync(event -> { .thenAcceptAsync(event -> {
if (event.getStatus() == PlayerResourcePackStatusEvent.Status.DECLINED if (event.getStatus() == PlayerResourcePackStatusEvent.Status.DECLINED
&& event.getPackInfo() != null && event.getPackInfo().getShouldForce() && event.getPackInfo() != null && event.getPackInfo().getShouldForce()
&& (!event.isOverwriteKick() || event.getPlayer() && (!event.isOverwriteKick() || event.getPlayer()
.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) .getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0)
) { ) {
event.getPlayer().disconnect(Component event.getPlayer().disconnect(Component
.translatable("multiplayer.requiredTexturePrompt.disconnect")); .translatable("multiplayer.requiredTexturePrompt.disconnect"));
} }
}); });
switch (status) { switch (status) {
case ACCEPTED: case ACCEPTED:
@ -1023,7 +1041,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
return queued != null return queued != null
&& queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER;
} }
/** /**
@ -1061,6 +1079,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
/** /**
* Return all the plugin message channels "known" to the client. * Return all the plugin message channels "known" to the client.
*
* @return the channels * @return the channels
*/ */
public Collection<String> getKnownChannels() { public Collection<String> getKnownChannels() {
@ -1085,7 +1104,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private final @Nullable VelocityRegisteredServer previousServer; private final @Nullable VelocityRegisteredServer previousServer;
ConnectionRequestBuilderImpl(RegisteredServer toConnect, ConnectionRequestBuilderImpl(RegisteredServer toConnect,
@Nullable VelocityServerConnection previousConnection) { @Nullable VelocityServerConnection previousConnection) {
this.toConnect = Preconditions.checkNotNull(toConnect, "info"); this.toConnect = Preconditions.checkNotNull(toConnect, "info");
this.previousServer = previousConnection == null ? null : previousConnection.getServer(); this.previousServer = previousConnection == null ? null : previousConnection.getServer();
} }
@ -1103,7 +1122,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return Optional.of(ConnectionRequestBuilder.Status.CONNECTION_IN_PROGRESS); return Optional.of(ConnectionRequestBuilder.Status.CONNECTION_IN_PROGRESS);
} }
if (connectedServer != null if (connectedServer != null
&& connectedServer.getServer().getServerInfo().equals(server.getServerInfo())) { && connectedServer.getServer().getServerInfo().equals(server.getServerInfo())) {
return Optional.of(ALREADY_CONNECTED); return Optional.of(ALREADY_CONNECTED);
} }
return Optional.empty(); return Optional.empty();

Datei anzeigen

@ -31,6 +31,7 @@ import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
public class ChatBuilder { public class ChatBuilder {
@ -43,11 +44,13 @@ public class ChatBuilder {
private @Nullable Player sender; private @Nullable Player sender;
private @Nullable Identity senderIdentity; private @Nullable Identity senderIdentity;
private @NotNull Instant timestamp;
private ChatType type = ChatType.CHAT; private ChatType type = ChatType.CHAT;
private ChatBuilder(ProtocolVersion version) { private ChatBuilder(ProtocolVersion version) {
this.version = version; this.version = version;
this.timestamp = Instant.now();
} }
public static ChatBuilder builder(ProtocolVersion version) { public static ChatBuilder builder(ProtocolVersion version) {
@ -115,6 +118,11 @@ public class ChatBuilder {
return this; return this;
} }
public ChatBuilder timestamp(Instant timestamp) {
this.timestamp = timestamp;
return this;
}
public ChatBuilder asServer() { public ChatBuilder asServer() {
this.sender = null; this.sender = null;
return this; return this;
@ -153,7 +161,7 @@ public class ChatBuilder {
} else { } else {
// Well crap // Well crap
if (message.startsWith("/")) { if (message.startsWith("/")) {
return new PlayerCommand(message.substring(1), ImmutableList.of(), Instant.now()); return new PlayerCommand(message.substring(1), ImmutableList.of(), timestamp);
} else { } else {
// This will produce an error on the server, but needs to be here. // This will produce an error on the server, but needs to be here.
return new PlayerChat(message); return new PlayerChat(message);

Datei anzeigen

@ -0,0 +1,157 @@
/*
* Copyright (C) 2018 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.packet.chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* A precisely ordered queue which allows for outside entries into the ordered queue through piggybacking timestamps.
*/
public class ChatQueue {
private final Object internalLock;
private final ConnectedPlayer player;
private CompletableFuture<WrappedPacket> packetFuture;
/**
* Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}.
*
* @param player the {@link ConnectedPlayer} to maintain the queue for.
*/
public ChatQueue(ConnectedPlayer player) {
this.player = player;
this.packetFuture = CompletableFuture.completedFuture(new WrappedPacket(Instant.EPOCH, null));
this.internalLock = new Object();
}
/**
* Queues a packet sent from the player - all packets must wait until this processes to send their packets.
* <br />
* This maintains order on the server-level for the client insertions of commands and messages. All entries are locked
* through an internal object lock.
*
* @param nextPacket the {@link CompletableFuture} which will provide the next-processed packet.
* @param timestamp the {@link Instant} timestamp of this packet so we can allow piggybacking.
*/
public void queuePacket(CompletableFuture<MinecraftPacket> nextPacket, Instant timestamp) {
synchronized (internalLock) { // wait for the lock to resolve - we don't want to drop packets
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
CompletableFuture<WrappedPacket> nextInLine = WrappedPacket.wrap(timestamp, nextPacket);
awaitChat(smc, this.packetFuture, nextInLine); // we await chat, binding `this.packetFuture` -> `nextInLine`
this.packetFuture = nextInLine;
}
}
/**
* Hijacks the latest sent packet's timestamp to provide an in-order packet without polling the physical, or prior
* packets sent through the stream.
*
* @param packet the {@link MinecraftPacket} to send.
* @param instantMapper the {@link InstantPacketMapper} which maps the prior timestamp and current packet to a new
* packet.
* @param <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, InstantPacketMapper<K, V> instantMapper) {
synchronized (internalLock) {
CompletableFuture<K> trueFuture = CompletableFuture.completedFuture(packet);
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
this.packetFuture = hijackCurrentPacket(smc, this.packetFuture, trueFuture, instantMapper);
}
}
private static BiConsumer<WrappedPacket, Throwable> writePacket(MinecraftConnection connection) {
return (wrappedPacket, throwable) -> {
if (wrappedPacket != null && !connection.isClosed()) {
wrappedPacket.write(connection);
}
};
}
private static <T extends MinecraftPacket> void awaitChat(
MinecraftConnection connection,
CompletableFuture<WrappedPacket> binder,
CompletableFuture<WrappedPacket> future
) {
// the binder will run -> then the future will get the `write packet` caller
binder.whenComplete((ignored1, ignored2) -> future.whenComplete(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)))
.whenCompleteAsync(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.
*
* @param <K> The base object type to map.
* @param <V> The resulting packet type.
*/
public interface InstantPacketMapper<K, V extends MinecraftPacket> {
/**
* Maps a value into a packet with it and a timestamp.
*
* @param nextInstant the {@link Instant} timestamp to use for tracking.
* @param currentObject the current item to map to the packet.
* @return The resulting packet from the mapping.
*/
V map(Instant nextInstant, K currentObject);
}
private static class WrappedPacket {
private final Instant timestamp;
private final MinecraftPacket packet;
private WrappedPacket(Instant timestamp, MinecraftPacket packet) {
this.timestamp = timestamp;
this.packet = packet;
}
public void write(MinecraftConnection connection) {
if (packet != null) {
connection.write(packet);
}
}
private static CompletableFuture<WrappedPacket> wrap(Instant timestamp,
CompletableFuture<MinecraftPacket> nextPacket) {
return nextPacket
.thenApply(pkt -> new WrappedPacket(timestamp, pkt))
.exceptionally(ignored -> new WrappedPacket(timestamp, null));
}
}
}