diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index a76f054e72..7cc6c4c804 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -318,6 +318,7 @@ public boolean handle(PluginMessagePacket packet) { + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); + player.getClientsideChannels().addAll(channels); List channelIdentifiers = new ArrayList<>(); for (String channel : channels) { try { @@ -331,6 +332,7 @@ public boolean handle(PluginMessagePacket packet) { new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { + player.getClientsideChannels().removeAll(PluginMessageUtil.getChannels(packet)); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -585,6 +587,10 @@ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnect if (!channels.isEmpty()) { serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); } + // Tell the server about this client's plugin message channels. + if (!player.getClientsideChannels().isEmpty()) { + serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getClientsideChannels())); + } // If we had plugin messages queued during login/FML handshake, send them now. PluginMessagePacket pm; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 8522dfce79..fc9f7f5d9c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -96,6 +96,7 @@ import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; import com.velocitypowered.proxy.util.TranslatableMapper; +import com.velocitypowered.proxy.util.collect.CappedSet; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; @@ -140,6 +141,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, VelocityInboundConnection { + private static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; @@ -167,6 +169,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; + private final Collection clientsideChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; private final ResourcePackHandler resourcePackHandler; @@ -197,6 +200,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.connectionPhase = connection.getType().getInitialClientPhase(); this.onlineMode = onlineMode; + this.clientsideChannels = CappedSet.create(MAX_CLIENTSIDE_PLUGIN_CHANNELS); if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { this.tabList = new VelocityTabList(this); @@ -1273,6 +1277,15 @@ public void setPhase(ClientConnectionPhase connectionPhase) { this.connectionPhase = connectionPhase; } + /** + * Return all the plugin message channels that registered by client. + * + * @return the channels + */ + public Collection getClientsideChannels() { + return clientsideChannels; + } + @Override public @Nullable IdentifiedKey getIdentifiedKey() { return playerKey; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java new file mode 100644 index 0000000000..692910d57f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.util.collect; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ForwardingSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * An unsynchronized collection that puts an upper bound on the size of the collection. + */ +public final class CappedSet extends ForwardingSet { + + private final Set delegate; + private final int upperSize; + + private CappedSet(Set delegate, int upperSize) { + this.delegate = delegate; + this.upperSize = upperSize; + } + + /** + * Creates a capped collection backed by a {@link HashSet}. + * + * @param maxSize the maximum size of the collection + * @param the type of elements in the collection + * @return the new collection + */ + public static Set create(int maxSize) { + return new CappedSet<>(new HashSet<>(), maxSize); + } + + @Override + protected Set delegate() { + return delegate; + } + + @Override + public boolean add(T element) { + if (this.delegate.size() >= upperSize) { + Preconditions.checkState(this.delegate.contains(element), + "collection is too large (%s >= %s)", + this.delegate.size(), this.upperSize); + return false; + } + return this.delegate.add(element); + } + + @Override + public boolean addAll(Collection collection) { + return this.standardAddAll(collection); + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java new file mode 100644 index 0000000000..2e118b4aca --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019-2021 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.util.collect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class CappedSetTest { + + @Test + void basicVerification() { + Collection coll = CappedSet.create(1); + assertTrue(coll.add("coffee"), "did not add single item"); + assertThrows(IllegalStateException.class, () -> coll.add("tea"), + "item was added to collection although it is too full"); + assertEquals(1, coll.size(), "collection grew in size unexpectedly"); + } + + @Test + void testAddAll() { + Set doesFill1 = ImmutableSet.of("coffee", "tea"); + Set doesFill2 = ImmutableSet.of("chocolate"); + Set overfill = ImmutableSet.of("Coke", "Pepsi"); + + Collection coll = CappedSet.create(3); + assertTrue(coll.addAll(doesFill1), "did not add items"); + assertTrue(coll.addAll(doesFill2), "did not add items"); + assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), + "items added to collection although it is too full"); + assertEquals(3, coll.size(), "collection grew in size unexpectedly"); + } + + @Test + void handlesSetBehaviorCorrectly() { + Set doesFill1 = ImmutableSet.of("coffee", "tea"); + Set doesFill2 = ImmutableSet.of("coffee", "chocolate"); + Set overfill = ImmutableSet.of("coffee", "Coke", "Pepsi"); + + Collection coll = CappedSet.create(3); + assertTrue(coll.addAll(doesFill1), "did not add items"); + assertTrue(coll.addAll(doesFill2), "did not add items"); + assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), + "items added to collection although it is too full"); + + assertFalse(coll.addAll(doesFill1), "added items?!?"); + + assertEquals(3, coll.size(), "collection grew in size unexpectedly"); + } +} \ No newline at end of file