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 ba6678b78..747006dce 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,5 +1,8 @@ package com.velocitypowered.proxy.connection.backend; +import static com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder.getBungeeCordChannel; + +import com.google.common.collect.ImmutableList; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; @@ -31,6 +34,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { private final VelocityServerConnection serverConn; private final ClientPlaySessionHandler playerSessionHandler; private final MinecraftConnection playerConnection; + private final BungeeCordMessageResponder bungeecordMessageResponder; private boolean exceptionTriggered = false; BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) { @@ -44,11 +48,18 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { "Initializing BackendPlaySessionHandler with no backing client play session handler!"); } this.playerSessionHandler = (ClientPlaySessionHandler) psh; + + this.bungeecordMessageResponder = new BungeeCordMessageResponder(server, + serverConn.getPlayer()); } @Override public void activated() { serverConn.getServer().addPlayer(serverConn.getPlayer()); + MinecraftConnection serverMc = serverConn.ensureConnected(); + serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), + ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())) + )); } @Override @@ -86,6 +97,10 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(PluginMessage packet) { + if (bungeecordMessageResponder.process(packet)) { + return true; + } + if (!serverConn.getPlayer().canForwardPluginMessage(serverConn.ensureConnected() .getProtocolVersion(), packet)) { return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java new file mode 100644 index 000000000..678c77e8f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -0,0 +1,329 @@ +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.util.UuidUtils; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.util.ByteBufDataInput; +import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; +import com.velocitypowered.proxy.server.VelocityRegisteredServer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.StringJoiner; +import net.kyori.text.serializer.legacy.LegacyComponentSerializer; + +class BungeeCordMessageResponder { + + private static final MinecraftChannelIdentifier MODERN_CHANNEL = MinecraftChannelIdentifier + .create("bungeecord", "main"); + private static final LegacyChannelIdentifier LEGACY_CHANNEL = + new LegacyChannelIdentifier("BungeeCord"); + + private final VelocityServer proxy; + private final ConnectedPlayer player; + + BungeeCordMessageResponder(VelocityServer proxy, ConnectedPlayer player) { + this.proxy = proxy; + this.player = player; + } + + private void processConnect(ByteBufDataInput in) { + String serverName = in.readUTF(); + proxy.getServer(serverName).ifPresent(server -> player.createConnectionRequest(server) + .fireAndForget()); + } + + private void processConnectOther(ByteBufDataInput in) { + String playerName = in.readUTF(); + String serverName = in.readUTF(); + + proxy.getPlayer(playerName).flatMap(player -> proxy.getServer(serverName)) + .ifPresent(server -> player.createConnectionRequest(server).fireAndForget()); + } + + private void processIp(ByteBufDataInput in) { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + out.writeUTF("IP"); + out.writeUTF(player.getRemoteAddress().getHostString()); + out.writeInt(player.getRemoteAddress().getPort()); + sendResponseOnConnection(buf); + } + + private void processPlayerCount(ByteBufDataInput in) { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + String target = in.readUTF(); + if (target.equals("ALL")) { + out.writeUTF("PlayerCount"); + out.writeUTF("ALL"); + out.writeInt(proxy.getPlayerCount()); + } else { + proxy.getServer(target).ifPresent(rs -> { + int playersOnServer = rs.getPlayersConnected().size(); + out.writeUTF("PlayerCount"); + out.writeUTF(rs.getServerInfo().getName()); + out.writeInt(playersOnServer); + }); + } + + if (buf.isReadable()) { + sendResponseOnConnection(buf); + } else { + buf.release(); + } + } + + private void processPlayerList(ByteBufDataInput in) { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + String target = in.readUTF(); + if (target.equals("ALL")) { + out.writeUTF("PlayerList"); + out.writeUTF("ALL"); + + StringJoiner joiner = new StringJoiner(", "); + for (Player online : proxy.getAllPlayers()) { + joiner.add(online.getUsername()); + } + out.writeUTF(joiner.toString()); + } else { + proxy.getServer(target).ifPresent(info -> { + out.writeUTF("PlayerList"); + out.writeUTF(info.getServerInfo().getName()); + + StringJoiner joiner = new StringJoiner(", "); + for (Player online : info.getPlayersConnected()) { + joiner.add(online.getUsername()); + } + out.writeUTF(joiner.toString()); + }); + } + + if (buf.isReadable()) { + sendResponseOnConnection(buf); + } else { + buf.release(); + } + } + + private void processGetServers() { + StringJoiner joiner = new StringJoiner(", "); + for (RegisteredServer server : proxy.getAllServers()) { + joiner.add(server.getServerInfo().getName()); + } + + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + out.writeUTF("GetServers"); + out.writeUTF(joiner.toString()); + + sendResponseOnConnection(buf); + } + + private void processMessage(ByteBufDataInput in) { + String target = in.readUTF(); + String message = in.readUTF(); + if (target.equals("ALL")) { + for (Player player : proxy.getAllPlayers()) { + player.sendMessage(LegacyComponentSerializer.INSTANCE.deserialize(message)); + } + } else { + proxy.getPlayer(target).ifPresent(player -> { + player.sendMessage(LegacyComponentSerializer.INSTANCE.deserialize(message)); + }); + } + } + + private void processGetServer() { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + out.writeUTF("GetServer"); + out.writeUTF(player.ensureAndGetCurrentServer().getServerInfo().getName()); + + sendResponseOnConnection(buf); + } + + private void processUuid() { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + out.writeUTF("UUID"); + out.writeUTF(UuidUtils.toUndashed(player.getUniqueId())); + + sendResponseOnConnection(buf); + } + + private void processUuidOther(ByteBufDataInput in) { + proxy.getPlayer(in.readUTF()).ifPresent(player -> { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + out.writeUTF("UUIDOther"); + out.writeUTF(player.getUsername()); + out.writeUTF(UuidUtils.toUndashed(player.getUniqueId())); + + sendResponseOnConnection(buf); + }); + } + + private void processServerIp(ByteBufDataInput in) { + proxy.getServer(in.readUTF()).ifPresent(info -> { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + out.writeUTF("ServerIP"); + out.writeUTF(info.getServerInfo().getName()); + out.writeUTF(info.getServerInfo().getAddress().getHostString()); + out.writeShort(info.getServerInfo().getAddress().getPort()); + + sendResponseOnConnection(buf); + }); + } + + private void processKick(ByteBufDataInput in) { + proxy.getPlayer(in.readUTF()).ifPresent(player -> { + String kickReason = in.readUTF(); + player.disconnect(LegacyComponentSerializer.INSTANCE.deserialize(kickReason)); + }); + } + + private ByteBuf prepareForwardMessage(ByteBufDataInput in) { + String channel = in.readUTF(); + short messageLength = in.readShort(); + + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput forwarded = new ByteBufDataOutput(buf); + forwarded.writeUTF(channel); + forwarded.writeShort(messageLength); + buf.writeBytes(in.unwrap().readSlice(messageLength)); + return buf; + } + + private void processForwardToPlayer(ByteBufDataInput in) { + proxy.getPlayer(in.readUTF()) + .flatMap(Player::getCurrentServer) + .ifPresent(server -> sendServerResponse(player, prepareForwardMessage(in))); + } + + private void processForwardToServer(ByteBufDataInput in) { + ByteBuf toForward = prepareForwardMessage(in); + String target = in.readUTF(); + if (target.equals("ALL")) { + ByteBuf unreleasableForward = Unpooled.unreleasableBuffer(toForward); + try { + for (RegisteredServer rs : proxy.getAllServers()) { + ((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL, unreleasableForward); + } + } finally { + toForward.release(); + } + } else { + proxy.getServer(target).ifPresent(rs -> ((VelocityRegisteredServer) rs) + .sendPluginMessage(LEGACY_CHANNEL, toForward)); + } + } + + // Note: this method will always release the buffer! + private void sendResponseOnConnection(ByteBuf buf) { + sendServerResponse(this.player, buf); + } + + static String getBungeeCordChannel(ProtocolVersion version) { + return version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ? MODERN_CHANNEL.getId() + : LEGACY_CHANNEL.getId(); + } + + // Note: this method will always release the buffer! + private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) { + MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected(); + String chan = getBungeeCordChannel(serverConnection.getProtocolVersion()); + + PluginMessage msg = null; + boolean released = false; + + try { + VelocityServerConnection vsc = player.getConnectedServer(); + if (vsc == null) { + return; + } + + MinecraftConnection serverConn = vsc.ensureConnected(); + msg = new PluginMessage(chan, buf); + serverConn.write(msg); + released = true; + } finally { + if (!released && msg != null) { + msg.release(); + } + } + } + + boolean process(PluginMessage message) { + if (!MODERN_CHANNEL.getId().equals(message.getChannel()) && !LEGACY_CHANNEL.getId() + .equals(message.getChannel())) { + return false; + } + + ByteBufDataInput in = new ByteBufDataInput(message.content()); + String subChannel = in.readUTF(); + switch (subChannel) { + case "ForwardToPlayer": + this.processForwardToPlayer(in); + break; + case "Forward": + this.processForwardToServer(in); + break; + case "Connect": + this.processConnect(in); + break; + case "ConnectOther": + this.processConnectOther(in); + break; + case "IP": + this.processIp(in); + break; + case "PlayerCount": + this.processPlayerCount(in); + break; + case "PlayerList": + this.processPlayerList(in); + break; + case "GetServers": + this.processGetServers(); + break; + case "Message": + this.processMessage(in); + break; + case "GetServer": + this.processGetServer(); + break; + case "UUID": + this.processUuid(); + break; + case "UUIDOther": + this.processUuidOther(in); + break; + case "ServerIP": + this.processServerIp(in); + break; + case "KickPlayer": + this.processKick(in); + break; + default: + // Do nothing, unknown command + break; + } + + return true; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index da9806e16..a7cf3ed82 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.protocol.packet.Handshake; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ServerLogin; import com.velocitypowered.proxy.server.VelocityRegisteredServer; +import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; @@ -210,12 +211,22 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, @Override public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + return sendPluginMessage(identifier, Unpooled.wrappedBuffer(data)); + } + + /** + * Sends a plugin message to the server through this connection. + * @param identifier the channel ID to use + * @param data the data + * @return whether or not the message was sent + */ + public boolean sendPluginMessage(ChannelIdentifier identifier, ByteBuf data) { Preconditions.checkNotNull(identifier, "identifier"); Preconditions.checkNotNull(data, "data"); MinecraftConnection mc = ensureConnected(); - PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data)); + PluginMessage message = new PluginMessage(identifier.getId(), data); mc.write(message); return true; } 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 1145e905b..d90fc2054 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 @@ -135,6 +135,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return Optional.ofNullable(connectedServer); } + /** + * 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 + */ + public VelocityServerConnection ensureAndGetCurrentServer() { + VelocityServerConnection con = this.connectedServer; + if (con == null) { + throw new IllegalStateException("Not connected to server!"); + } + return con; + } + @Override public GameProfile getGameProfile() { return profile; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataInput.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataInput.java new file mode 100644 index 000000000..43b6d44aa --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataInput.java @@ -0,0 +1,110 @@ +package com.velocitypowered.proxy.protocol.util; + +import com.google.common.io.ByteArrayDataInput; +import io.netty.buffer.ByteBuf; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; + +/** + * A wrapper around {@link io.netty.buffer.ByteBuf} that implements the exception-free + * {@link ByteArrayDataInput} interface from Guava. + */ +public class ByteBufDataInput implements ByteArrayDataInput, DataInput { + + private final ByteBuf in; + + /** + * Creates a new ByteBufDataInput instance. The ByteBufDataInput simply "borrows" the ByteBuf + * while it is in use. + * + * @param buf the buffer to read from + */ + public ByteBufDataInput(ByteBuf buf) { + this.in = buf; + } + + public ByteBuf unwrap() { + return in; + } + + @Override + public void readFully(byte[] b) { + in.readBytes(b); + } + + @Override + public void readFully(byte[] b, int off, int len) { + in.readBytes(b, off, len); + } + + @Override + public int skipBytes(int n) { + in.skipBytes(n); + return n; + } + + @Override + public boolean readBoolean() { + return in.readBoolean(); + } + + @Override + public byte readByte() { + return in.readByte(); + } + + @Override + public int readUnsignedByte() { + return in.readUnsignedByte() & 0xFF; + } + + @Override + public short readShort() { + return in.readShort(); + } + + @Override + public int readUnsignedShort() { + return in.readUnsignedShort(); + } + + @Override + public char readChar() { + return in.readChar(); + } + + @Override + public int readInt() { + return in.readInt(); + } + + @Override + public long readLong() { + return in.readLong(); + } + + @Override + public float readFloat() { + return in.readFloat(); + } + + @Override + public double readDouble() { + return in.readDouble(); + } + + @Override + public String readLine() { + throw new UnsupportedOperationException(); + } + + @Override + public String readUTF() { + try { + return DataInputStream.readUTF(this); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java new file mode 100644 index 000000000..b894525ab --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java @@ -0,0 +1,105 @@ +package com.velocitypowered.proxy.protocol.util; + +import com.google.common.io.ByteArrayDataOutput; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * A {@link DataOutput} equivalent to {@link ByteBufDataInput}. + */ +public class ByteBufDataOutput extends OutputStream implements DataOutput, ByteArrayDataOutput { + + private final ByteBuf buf; + private final DataOutputStream utf8out; + + public ByteBufDataOutput(ByteBuf buf) { + this.buf = buf; + this.utf8out = new DataOutputStream(this); + } + + @Override + public byte[] toByteArray() { + return ByteBufUtil.getBytes(buf); + } + + @Override + public void write(int b) { + buf.writeByte(b); + } + + @Override + public void write(byte[] b) { + buf.writeBytes(b); + } + + @Override + public void write(byte[] b, int off, int len) { + buf.writeBytes(b, off, len); + } + + @Override + public void writeBoolean(boolean v) { + buf.writeBoolean(v); + } + + @Override + public void writeByte(int v) { + buf.writeByte(v); + } + + @Override + public void writeShort(int v) { + buf.writeShort(v); + } + + @Override + public void writeChar(int v) { + buf.writeChar(v); + } + + @Override + public void writeInt(int v) { + buf.writeInt(v); + } + + @Override + public void writeLong(long v) { + buf.writeLong(v); + } + + @Override + public void writeFloat(float v) { + buf.writeFloat(v); + } + + @Override + public void writeDouble(double v) { + buf.writeDouble(v); + } + + @Override + public void writeBytes(String s) { + buf.writeCharSequence(s, StandardCharsets.US_ASCII); + } + + @Override + public void writeChars(String s) { + for (char c : s.toCharArray()) { + buf.writeChar(c); + } + } + + @Override + public void writeUTF(String s) { + try { + this.utf8out.writeUTF(s); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index cf6694cb4..dc8f1687d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -18,12 +18,16 @@ import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; @@ -119,8 +123,18 @@ public class VelocityRegisteredServer implements RegisteredServer { @Override public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + return sendPluginMessage(identifier, Unpooled.wrappedBuffer(data)); + } + + /** + * Sends a plugin message to the server through this connection. + * @param identifier the channel ID to use + * @param data the data + * @return whether or not the message was sent + */ + public boolean sendPluginMessage(ChannelIdentifier identifier, ByteBuf data) { for (ConnectedPlayer player : players) { - ServerConnection connection = player.getConnectedServer(); + VelocityServerConnection connection = player.getConnectedServer(); if (connection != null && connection.getServerInfo().equals(serverInfo)) { return connection.sendPluginMessage(identifier, data); }