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..dbedce09a 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 @@ -31,6 +31,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,6 +45,9 @@ 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 @@ -86,6 +90,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..45c697628 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -0,0 +1,325 @@ +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +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.ByteBufUtil; +import io.netty.buffer.Unpooled; +import java.util.StringJoiner; +import java.util.stream.Collectors; +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).ifPresent(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()); + sendResponse(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()) { + sendResponse(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()) { + sendResponse(buf); + } else { + buf.release(); + } + } + + private void processGetServers(ByteBufDataInput in) { + 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()); + + sendResponse(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(ByteBufDataInput in) { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + out.writeUTF("GetServer"); + out.writeUTF(player.ensureAndGetCurrentServer().getServerInfo().getName()); + + sendResponse(buf); + } + + private void processUuid(ByteBufDataInput in) { + ByteBuf buf = Unpooled.buffer(); + ByteBufDataOutput out = new ByteBufDataOutput(buf); + + out.writeUTF("UUID"); + out.writeUTF(UuidUtils.toUndashed(player.getUniqueId())); + + sendResponse(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())); + + sendResponse(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()); + + sendResponse(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 -> sendResponse(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 sendResponse(ByteBuf buf) { + sendResponse(this.player, buf); + } + + // Note: this method will always release the buffer! + private static void sendResponse(ConnectedPlayer player, ByteBuf buf) { + MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected(); + String chan = serverConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) + >= 0 ? MODERN_CHANNEL.getId() : LEGACY_CHANNEL.getId(); + + 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(); + } + } + } + + public 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(); + if (subChannel.equals("ForwardToPlayer")) { + this.processForwardToPlayer(in); + } + if (subChannel.equals("Forward")) { + this.processForwardToServer(in); + } + if (subChannel.equals("Connect")) { + this.processConnect(in); + } + if (subChannel.equals("ConnectOther")) { + this.processConnectOther(in); + } + if (subChannel.equals("IP")) { + this.processIp(in); + } + if (subChannel.equals("PlayerCount")) { + this.processPlayerCount(in); + } + if (subChannel.equals("PlayerList")) { + this.processPlayerList(in); + } + if (subChannel.equals("GetServers")) { + this.processGetServers(in); + } + if (subChannel.equals("Message")) { + this.processMessage(in); + } + if (subChannel.equals("GetServer")) { + this.processGetServer(in); + } + if (subChannel.equals("UUID")) { + this.processUuid(in); + } + if (subChannel.equals("UUIDOther")) { + this.processUuidOther(in); + } + if (subChannel.equals("ServerIP")) { + this.processServerIp(in); + } + if (subChannel.equals("KickPlayer")) { + this.processKick(in); + } + + 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..9f0903469 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,16 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, @Override public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + return sendPluginMessage(identifier, Unpooled.wrappedBuffer(data)); + } + + 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 c012e8fcb..0483ce352 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,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return Optional.ofNullable(connectedServer); } + 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..e999d5e7c 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,12 @@ public class VelocityRegisteredServer implements RegisteredServer { @Override public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + return sendPluginMessage(identifier, Unpooled.wrappedBuffer(data)); + } + + 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); }