diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 08bf0c440..92871b30e 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -1,6 +1,7 @@ package com.velocitypowered.api.proxy; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.server.ServerInfo; import com.velocitypowered.api.util.MessagePosition; import net.kyori.text.Component; @@ -12,7 +13,7 @@ import java.util.UUID; /** * Represents a player who is connected to the proxy. */ -public interface Player extends CommandSource, InboundConnection { +public interface Player extends CommandSource, InboundConnection, ChannelMessageSource { /** * Returns the player's current username. * @return the username diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index b35fe127c..433c61097 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -4,6 +4,7 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.plugin.PluginManager; +import com.velocitypowered.api.proxy.messages.ChannelRegistrar; import com.velocitypowered.api.scheduler.Scheduler; import com.velocitypowered.api.server.ServerInfo; @@ -101,4 +102,10 @@ public interface ProxyServer { * @return the scheduler instance */ Scheduler getScheduler(); + + /** + * Gets the {@link ChannelRegistrar} instance. + * @return the channel registrar + */ + ChannelRegistrar getChannelRegistrar(); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java index 8753019d4..4e18fd949 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java @@ -1,11 +1,12 @@ package com.velocitypowered.api.proxy; +import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.server.ServerInfo; /** * Represents a connection to a backend server from the proxy for a client. */ -public interface ServerConnection { +public interface ServerConnection extends ChannelMessageSource { ServerInfo getServerInfo(); Player getPlayer(); diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelIdentifier.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelIdentifier.java new file mode 100644 index 000000000..0af1eeae2 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelIdentifier.java @@ -0,0 +1,8 @@ +package com.velocitypowered.api.proxy.messages; + +/** + * Represents a kind of channel identifier. + */ +public interface ChannelIdentifier { + String getId(); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSource.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSource.java new file mode 100644 index 000000000..1aaeae943 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSource.java @@ -0,0 +1,4 @@ +package com.velocitypowered.api.proxy.messages; + +public interface ChannelMessageSource { +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java new file mode 100644 index 000000000..412e172df --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java @@ -0,0 +1,11 @@ +package com.velocitypowered.api.proxy.messages; + +/** + * Represents an interface to register and unregister {@link MessageHandler} instances for handling plugin messages from + * the client or the server. + */ +public interface ChannelRegistrar { + void register(MessageHandler handler, ChannelIdentifier... identifiers); + + void unregister(ChannelIdentifier... identifiers); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelSide.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelSide.java new file mode 100644 index 000000000..f27faf71b --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelSide.java @@ -0,0 +1,6 @@ +package com.velocitypowered.api.proxy.messages; + +public enum ChannelSide { + FROM_SERVER, + FROM_CLIENT +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/LegacyChannelIdentifier.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/LegacyChannelIdentifier.java new file mode 100644 index 000000000..a5a345cfd --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/LegacyChannelIdentifier.java @@ -0,0 +1,48 @@ +package com.velocitypowered.api.proxy.messages; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +import java.util.Objects; + +/** + * Reperesents a legacy channel identifier (for Minecraft 1.12 and below). For modern 1.13 plugin messages, please see + * {@link MinecraftChannelIdentifier}. This class is immutable and safe for multi-threaded use. + */ +public final class LegacyChannelIdentifier implements ChannelIdentifier { + private final String name; + + public LegacyChannelIdentifier(String name) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "provided name is empty"); + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "LegacyChannelIdentifier{" + + "name='" + name + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LegacyChannelIdentifier that = (LegacyChannelIdentifier) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String getId() { + return name; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/MessageHandler.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/MessageHandler.java new file mode 100644 index 000000000..74fe81f82 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/MessageHandler.java @@ -0,0 +1,10 @@ +package com.velocitypowered.api.proxy.messages; + +public interface MessageHandler { + ForwardStatus handle(ChannelMessageSource source, ChannelSide side, byte[] data); + + enum ForwardStatus { + FORWARD, + HANDLED + } +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java new file mode 100644 index 000000000..c171fee66 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java @@ -0,0 +1,69 @@ +package com.velocitypowered.api.proxy.messages; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Represents a Minecraft 1.13+ channel identifier. This class is immutable and safe for multi-threaded use. + */ +public final class MinecraftChannelIdentifier implements ChannelIdentifier { + private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]+"); + + private final String namespace; + private final String name; + + private MinecraftChannelIdentifier(String namespace, String name) { + this.namespace = namespace; + this.name = name; + } + + public static MinecraftChannelIdentifier forDefaultNamespace(String name) { + return new MinecraftChannelIdentifier("minecraft", name); + } + + public static MinecraftChannelIdentifier create(String namespace, String name) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "namespace is null or empty"); + Preconditions.checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(), "namespace is not valid"); + Preconditions.checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(), "name is not valid"); + return new MinecraftChannelIdentifier(namespace, name); + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "MinecraftChannelIdentifier{" + + "namespace='" + namespace + '\'' + + ", name='" + name + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MinecraftChannelIdentifier that = (MinecraftChannelIdentifier) o; + return Objects.equals(namespace, that.namespace) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(namespace, name); + } + + @Override + public String getId() { + return namespace + ":" + name; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index c474019e6..5a9702c5f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -21,6 +21,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.http.NettyHttpClient; import com.velocitypowered.proxy.command.VelocityCommandManager; +import com.velocitypowered.proxy.messages.VelocityChannelRegistrar; import com.velocitypowered.proxy.plugin.VelocityEventManager; import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.plugin.VelocityPluginManager; @@ -84,6 +85,7 @@ public class VelocityServer implements ProxyServer { private Ratelimiter ipAttemptLimiter; private VelocityEventManager eventManager; private VelocityScheduler scheduler; + private VelocityChannelRegistrar channelRegistrar; private VelocityServer() { commandManager.register(new VelocityCommand(), "velocity"); @@ -137,6 +139,7 @@ public class VelocityServer implements ProxyServer { httpClient = new NettyHttpClient(this); eventManager = new VelocityEventManager(pluginManager); scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM); + channelRegistrar = new VelocityChannelRegistrar(); loadPlugins(); try { @@ -304,4 +307,9 @@ public class VelocityServer implements ProxyServer { public VelocityScheduler getScheduler() { return scheduler; } + + @Override + public VelocityChannelRegistrar getChannelRegistrar() { + return channelRegistrar; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index b48aa2dcd..29e24b2f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -1,6 +1,8 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.messages.ChannelSide; +import com.velocitypowered.api.proxy.messages.MessageHandler; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -65,7 +67,11 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return; } - connection.getPlayer().getConnection().write(pm); + MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage( + connection, ChannelSide.FROM_SERVER, pm); + if (status == MessageHandler.ForwardStatus.FORWARD) { + connection.getPlayer().getConnection().write(pm); + } } else { // Just forward the packet on. We don't have anything to handle at this time. connection.getPlayer().getConnection().write(packet); 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 2f185f328..0cce4bfc4 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 @@ -1,6 +1,8 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.proxy.messages.ChannelSide; +import com.velocitypowered.api.proxy.messages.MessageHandler; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; @@ -220,8 +222,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return; } - // We're going to forward on the original packet. - player.getConnectedServer().getMinecraftConnection().write(packet); + MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage( + player, ChannelSide.FROM_CLIENT, packet); + if (status == MessageHandler.ForwardStatus.FORWARD) { + // We're going to forward on the original packet. + player.getConnectedServer().getMinecraftConnection().write(packet); + } } public Set getClientPluginMsgChannels() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java new file mode 100644 index 000000000..8b74c4917 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java @@ -0,0 +1,48 @@ +package com.velocitypowered.proxy.messages; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.messages.*; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class VelocityChannelRegistrar implements ChannelRegistrar { + private static final Logger logger = LogManager.getLogger(VelocityChannelRegistrar.class); + private final Map handlers = new ConcurrentHashMap<>(); + + @Override + public void register(MessageHandler handler, ChannelIdentifier... identifiers) { + for (ChannelIdentifier identifier : identifiers) { + Preconditions.checkArgument(identifier instanceof LegacyChannelIdentifier || identifier instanceof MinecraftChannelIdentifier, + "identifier is unknown"); + } + + for (ChannelIdentifier identifier : identifiers) { + handlers.put(identifier.getId(), handler); + } + } + + public MessageHandler.ForwardStatus handlePluginMessage(ChannelMessageSource source, ChannelSide side, PluginMessage message) { + MessageHandler handler = handlers.get(message.getChannel()); + if (handler == null) { + // Nothing we can do. + return MessageHandler.ForwardStatus.FORWARD; + } + + try { + return handler.handle(source, side, message.getData()); + } catch (Exception e) { + logger.info("Unable to handle plugin message on channel {} for {}", message.getChannel(), source); + // In case of doubt, do not forward the message on. + return MessageHandler.ForwardStatus.HANDLED; + } + } + + @Override + public void unregister(ChannelIdentifier... identifiers) { + + } +}