From 2d2c38e120940aa2e92e1676f7efed7fa03c853e Mon Sep 17 00:00:00 2001 From: Tim203 Date: Sun, 7 Nov 2021 14:51:37 +0100 Subject: [PATCH] Moved some common data handling logic to the common module --- .../floodgate/addon/data/BungeeDataAddon.java | 2 +- .../addon/data/BungeeProxyDataHandler.java | 123 +++------- .../addon/data/CommonDataHandler.java | 179 +++++++++++++++ .../floodgate/addon/data/PacketBlocker.java | 4 + .../player/FloodgateHandshakeHandler.java | 111 +++++---- .../floodgate/addon/data/SpigotDataAddon.java | 14 +- .../addon/data/SpigotDataHandler.java | 216 ++++++++---------- .../geysermc/floodgate/util/ClassNames.java | 12 + .../addon/data/VelocityProxyDataHandler.java | 124 +++------- 9 files changed, 434 insertions(+), 351 deletions(-) create mode 100644 common/src/main/java/org/geysermc/floodgate/addon/data/CommonDataHandler.java diff --git a/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeDataAddon.java b/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeDataAddon.java index cb794861..96cb133e 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeDataAddon.java +++ b/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeDataAddon.java @@ -79,7 +79,7 @@ public class BungeeDataAddon implements InjectorAddon { channel.pipeline().addBefore( packetHandler, "floodgate_data_handler", - new BungeeProxyDataHandler(config, handshakeHandler, blocker, kickMessageAttribute) + new BungeeProxyDataHandler(handshakeHandler, config, kickMessageAttribute, blocker) ); } diff --git a/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeProxyDataHandler.java b/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeProxyDataHandler.java index 2e1908b5..ff99dec1 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeProxyDataHandler.java +++ b/bungee/src/main/java/org/geysermc/floodgate/addon/data/BungeeProxyDataHandler.java @@ -27,30 +27,22 @@ package org.geysermc.floodgate.addon.data; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.collect.Queues; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.Field; import java.net.InetSocketAddress; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import lombok.RequiredArgsConstructor; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.PacketWrapper; import net.md_5.bungee.protocol.packet.Handshake; -import org.geysermc.floodgate.api.handshake.HandshakeData; import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.player.FloodgateHandshakeHandler; -import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.ReflectionUtils; @SuppressWarnings("ConstantConditions") -@RequiredArgsConstructor -public class BungeeProxyDataHandler extends ChannelInboundHandlerAdapter { +public class BungeeProxyDataHandler extends CommonDataHandler { private static final Field HANDLER; private static final Field CHANNEL_WRAPPER; @@ -63,100 +55,45 @@ public class BungeeProxyDataHandler extends ChannelInboundHandlerAdapter { checkNotNull(CHANNEL_WRAPPER, "ChannelWrapper field cannot be null"); } - private final ProxyFloodgateConfig config; - private final FloodgateHandshakeHandler handler; - private final PacketBlocker blocker; - private final AttributeKey kickMessageAttribute; - - private final Queue packetQueue = Queues.newConcurrentLinkedQueue(); + public BungeeProxyDataHandler( + FloodgateHandshakeHandler handshakeHandler, + ProxyFloodgateConfig config, + AttributeKey kickMessageAttribute, + PacketBlocker blocker) { + super(handshakeHandler, config, kickMessageAttribute, blocker); + } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - // prevent other packets from being handled while we handle the handshake packet - if (!packetQueue.isEmpty()) { - packetQueue.add(msg); - return; - } + protected void setNewIp(Channel channel, InetSocketAddress newIp) { + HandlerBoss handlerBoss = ctx.pipeline().get(HandlerBoss.class); + // InitialHandler extends PacketHandler and implements PendingConnection + InitialHandler connection = ReflectionUtils.getCastedValue(handlerBoss, HANDLER); + ChannelWrapper channelWrapper = ReflectionUtils.getCastedValue(connection, CHANNEL_WRAPPER); + channelWrapper.setRemoteAddress(newIp); + } + + @Override + protected void setHostname(Object wrapperWithHandshake, String hostname) { + PacketWrapper wrapper = (PacketWrapper) wrapperWithHandshake; + Handshake handshake = (Handshake) wrapper.packet; + handshake.setHost(hostname); + } + + @Override + public boolean channelRead(Object msg) { if (msg instanceof PacketWrapper) { DefinedPacket packet = ((PacketWrapper) msg).packet; // we're only interested in the Handshake packet if (packet instanceof Handshake) { - blocker.enable(); - packetQueue.add(msg); + handle(msg, ((Handshake) packet).getHost()); - handleHandshake(ctx, (Handshake) packet).thenRun(() -> { - Object queuedPacket; - while ((queuedPacket = packetQueue.poll()) != null) { - ctx.fireChannelRead(queuedPacket); - } - ctx.pipeline().remove(this); - blocker.disable(); - }); - return; + // otherwise, it'll get read twice. once by the packet queue and once by this method + return false; } } - ctx.fireChannelRead(msg); - } - - private CompletableFuture handleHandshake(ChannelHandlerContext ctx, Handshake packet) { - String data = packet.getHost(); - - return handler.handle(ctx.channel(), data).thenAccept(result -> { - HandshakeData handshakeData = result.getHandshakeData(); - - // we'll change the IP address from the proxy to the real IP of the client very early on - // so that almost every plugin will use the real IP of the client - InetSocketAddress newIp = result.getNewIp(ctx.channel()); - if (newIp != null) { - HandlerBoss handlerBoss = ctx.pipeline().get(HandlerBoss.class); - // InitialHandler extends PacketHandler and implements PendingConnection - InitialHandler connection = ReflectionUtils.getCastedValue(handlerBoss, HANDLER); - - ChannelWrapper channelWrapper = - ReflectionUtils.getCastedValue(connection, CHANNEL_WRAPPER); - - channelWrapper.setRemoteAddress(newIp); - } - - packet.setHost(handshakeData.getHostname()); - - if (handshakeData.getDisconnectReason() != null) { - ctx.channel().attr(kickMessageAttribute).set(handshakeData.getDisconnectReason()); - return; - } - - switch (result.getResultType()) { - case EXCEPTION: - ctx.channel().attr(kickMessageAttribute) - .set(Constants.INTERNAL_ERROR_MESSAGE); - break; - case DECRYPT_ERROR: - ctx.channel().attr(kickMessageAttribute) - .set(config.getDisconnect().getInvalidKey()); - break; - case INVALID_DATA_LENGTH: - ctx.channel().attr(kickMessageAttribute) - .set(config.getDisconnect().getInvalidArgumentsLength()); - break; - default: - break; - } - }).handle((v, error) -> { - if (error != null) { - error.printStackTrace(); - } - return v; - }); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - if (config.isDebug()) { - cause.printStackTrace(); - } + return true; } } diff --git a/common/src/main/java/org/geysermc/floodgate/addon/data/CommonDataHandler.java b/common/src/main/java/org/geysermc/floodgate/addon/data/CommonDataHandler.java new file mode 100644 index 00000000..e24dbcde --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/addon/data/CommonDataHandler.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.addon.data; + +import com.google.common.collect.Queues; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.AttributeKey; +import it.unimi.dsi.fastutil.Pair; +import java.net.InetSocketAddress; +import java.util.Queue; +import lombok.RequiredArgsConstructor; +import org.geysermc.floodgate.api.handshake.HandshakeData; +import org.geysermc.floodgate.config.FloodgateConfig; +import org.geysermc.floodgate.player.FloodgateHandshakeHandler; +import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult; +import org.geysermc.floodgate.util.Constants; + +@RequiredArgsConstructor +public abstract class CommonDataHandler extends ChannelInboundHandlerAdapter { + protected final FloodgateHandshakeHandler handshakeHandler; + protected final FloodgateConfig config; + protected final AttributeKey kickMessageAttribute; + protected final PacketBlocker blocker; + + protected final Queue packetQueue = Queues.newConcurrentLinkedQueue(); + protected ChannelHandlerContext ctx; + + protected abstract void setNewIp(Channel channel, InetSocketAddress newIp); + + protected abstract void setHostname(Object handshakePacket, String hostname); + + protected abstract boolean channelRead(Object packet) throws Exception; + + protected boolean shouldRemoveHandler(HandshakeResult result) { + return true; + } + + protected boolean shouldCallFireRead(Object queuedPacket) { + return true; + } + + protected void handle(Object handshakePacket, String hostname) { + Pair separated = handshakeHandler.separateHostname(hostname); + + if (separated.left() == null) { + // not a Floodgate player + removeSelf(); + return; + } + + blocker.enable(); + packetQueue.add(handshakePacket); + + Channel channel = ctx.channel(); + + handshakeHandler + .handle(channel, separated.left(), separated.right()) + .thenApply(result -> { + HandshakeData handshakeData = result.getHandshakeData(); + + // we'll change the IP address to the real IP of the client very early on + // so that almost every plugin will use the real IP of the client + InetSocketAddress newIp = result.getNewIp(channel); + if (newIp != null) { + setNewIp(channel, newIp); + } + + setHostname(handshakePacket, handshakeData.getHostname()); + + if (handshakeData.shouldDisconnect()) { + setKickMessage(handshakeData.getDisconnectReason()); + return shouldRemoveHandler(result); + } + + switch (result.getResultType()) { + case EXCEPTION: + setKickMessage(Constants.INTERNAL_ERROR_MESSAGE); + break; + case DECRYPT_ERROR: + setKickMessage(config.getDisconnect().getInvalidKey()); + break; + case INVALID_DATA_LENGTH: + setKickMessage(config.getDisconnect().getInvalidArgumentsLength()); + break; + default: + break; + } + return shouldRemoveHandler(result); + }).handle((shouldRemove, error) -> { + if (error != null) { + error.printStackTrace(); + } + disablePacketQueue(shouldRemove); + return shouldRemove; + }); + } + + protected void disablePacketQueue(boolean removeSelf) { + Object queuedPacket; + while ((queuedPacket = packetQueue.poll()) != null) { + if (shouldCallFireRead(queuedPacket)) { + ctx.fireChannelRead(queuedPacket); + } + } + if (removeSelf) { + removeSelf(); + } + blocker.disable(); + } + + protected void removeSelf() { + ctx.pipeline().remove(this); + } + + protected final void setKickMessage(String message) { + ctx.channel().attr(kickMessageAttribute).set(message); + } + + protected final String getKickMessage() { + return ctx.channel().attr(kickMessageAttribute).get(); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + this.ctx = ctx; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object packet) { + // prevent other packets from being handled while we handle the handshake packet + if (!packetQueue.isEmpty()) { + packetQueue.add(packet); + return; + } + + try { + if (channelRead(packet)) { + ctx.fireChannelRead(packet); + } + } catch (Exception exception) { + exception.printStackTrace(); + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + if (config.isDebug()) { + cause.printStackTrace(); + } + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/addon/data/PacketBlocker.java b/common/src/main/java/org/geysermc/floodgate/addon/data/PacketBlocker.java index 1331b417..e1e797ec 100644 --- a/common/src/main/java/org/geysermc/floodgate/addon/data/PacketBlocker.java +++ b/common/src/main/java/org/geysermc/floodgate/addon/data/PacketBlocker.java @@ -63,6 +63,10 @@ public class PacketBlocker extends ChannelInboundHandlerAdapter { ctx.pipeline().remove(this); } + public boolean enabled() { + return blockPackets; + } + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (blockPackets || !packetQueue.isEmpty()) { diff --git a/common/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java b/common/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java index 2243d6ca..507772a6 100644 --- a/common/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java +++ b/common/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java @@ -30,8 +30,6 @@ import static org.geysermc.floodgate.player.FloodgateHandshakeHandler.ResultType import static org.geysermc.floodgate.util.BedrockData.EXPECTED_LENGTH; import com.google.common.base.Charsets; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import it.unimi.dsi.fastutil.Pair; @@ -40,12 +38,10 @@ import java.net.InetSocketAddress; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeUnit; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NonNull; -import lombok.RequiredArgsConstructor; import org.geysermc.floodgate.addon.data.HandshakeDataImpl; import org.geysermc.floodgate.addon.data.HandshakeHandlersImpl; import org.geysermc.floodgate.api.SimpleFloodgateApi; @@ -56,15 +52,11 @@ import org.geysermc.floodgate.api.player.PropertyKey; import org.geysermc.floodgate.config.FloodgateConfigHolder; import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.skin.SkinUploadManager; -import org.geysermc.floodgate.time.TimeSyncer; import org.geysermc.floodgate.util.BedrockData; -import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.InvalidFormatException; import org.geysermc.floodgate.util.LinkedPlayer; -import org.geysermc.floodgate.util.TimeSyncerHolder; import org.geysermc.floodgate.util.Utils; -@RequiredArgsConstructor public final class FloodgateHandshakeHandler { private final HandshakeHandlersImpl handshakeHandlers; private final SimpleFloodgateApi api; @@ -74,36 +66,79 @@ public final class FloodgateHandshakeHandler { private final AttributeKey playerAttribute; private final FloodgateLogger logger; - public CompletableFuture handle( - @NonNull Channel channel, - @NonNull String originalHostname) { + public FloodgateHandshakeHandler( + HandshakeHandlersImpl handshakeHandlers, + SimpleFloodgateApi api, + FloodgateCipher cipher, + FloodgateConfigHolder configHolder, + SkinUploadManager skinUploadManager, + AttributeKey playerAttribute, + FloodgateLogger logger) { - String[] split = originalHostname.split("\0"); - String data = null; + this.handshakeHandlers = handshakeHandlers; + this.api = api; + this.cipher = cipher; + this.configHolder = configHolder; + this.skinUploadManager = skinUploadManager; + this.playerAttribute = playerAttribute; + this.logger = logger; + } + + /** + * Separates the Floodgate data from the hostname + * + * @param hostname the string to look in + * @return The first string is the Floodgate data (or null if there isn't) and the second string + * is the hostname without the Floodgate. + */ + public Pair separateHostname(@NonNull String hostname) { + String[] hostnameItems = hostname.split("\0"); + String floodgateData = null; StringBuilder hostnameBuilder = new StringBuilder(); - for (String value : split) { - if (data == null && FloodgateCipher.hasHeader(value)) { - data = value; + for (String value : hostnameItems) { + if (floodgateData == null && FloodgateCipher.hasHeader(value)) { + floodgateData = value; continue; } hostnameBuilder.append(value).append('\0'); } // hostname now doesn't have Floodgate data anymore if it had - String hostname = hostnameBuilder.toString(); + return Pair.of(floodgateData, hostnameBuilder.toString()); + } - if (data == null) { - return CompletableFuture.completedFuture( - callHandlerAndReturnResult(NOT_FLOODGATE_DATA, channel, null, hostname) - ); - } + public CompletableFuture handle( + @NonNull Channel channel, + @NonNull String floodgateDataString, + @NonNull String hostname) { - byte[] floodgateData = data.getBytes(Charsets.UTF_8); + byte[] floodgateData = floodgateDataString.getBytes(Charsets.UTF_8); return CompletableFuture.supplyAsync(() -> { + + String decrypted; + try { + // the actual decryption of the data + decrypted = cipher.decryptToString(floodgateData); + } catch (InvalidFormatException e) { + // when the Floodgate format couldn't be found + throw callHandlerAndReturnResult( + NOT_FLOODGATE_DATA, + channel, null, hostname + ); + } catch (Exception e) { + // all the other exceptions are caused by invalid/tempered Floodgate data + if (configHolder.get().isDebug()) { + e.printStackTrace(); + } + + throw callHandlerAndReturnResult( + ResultType.DECRYPT_ERROR, + channel, null, hostname + ); + } + try { - // actual decryption - String decrypted = cipher.decryptToString(floodgateData); BedrockData bedrockData = BedrockData.fromString(decrypted); if (bedrockData.getDataLength() != EXPECTED_LENGTH) { @@ -122,22 +157,6 @@ public final class FloodgateHandshakeHandler { // let's check if there is a link return bedrockData; - } catch (InvalidFormatException formatException) { - // only header exceptions should return 'not floodgate data', - // all the other format exceptions are because of invalid/tempered Floodgate data - if (formatException.isHeader()) { - throw callHandlerAndReturnResult( - NOT_FLOODGATE_DATA, - channel, null, hostname - ); - } - - formatException.printStackTrace(); - - throw callHandlerAndReturnResult( - ResultType.DECRYPT_ERROR, - channel, null, hostname - ); } catch (Exception exception) { if (exception instanceof HandshakeResult) { throw (HandshakeResult) exception; @@ -174,7 +193,12 @@ public final class FloodgateHandshakeHandler { }); } - private HandshakeResult handlePart2(Channel channel, String hostname, BedrockData bedrockData, LinkedPlayer linkedPlayer) { + private HandshakeResult handlePart2( + Channel channel, + String hostname, + BedrockData bedrockData, + LinkedPlayer linkedPlayer) { + try { HandshakeData handshakeData = new HandshakeDataImpl( channel, true, bedrockData.clone(), configHolder.get(), @@ -252,7 +276,8 @@ public final class FloodgateHandshakeHandler { .thenApply(link -> new ObjectObjectImmutablePair<>(data, link)) .handle((result, error) -> { if (error != null) { - logger.error("The player linking implementation returned an error", error.getCause()); + logger.error("The player linking implementation returned an error", + error.getCause()); return new ObjectObjectImmutablePair<>(data, null); } return result; diff --git a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataAddon.java b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataAddon.java index c042dbc3..8db20008 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataAddon.java +++ b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataAddon.java @@ -42,26 +42,24 @@ public final class SpigotDataAddon implements InjectorAddon { @Inject private SimpleFloodgateApi api; @Inject private FloodgateLogger logger; - @Inject - @Named("packetDecoder") - private String packetDecoder; - @Inject @Named("packetHandler") private String packetHandlerName; + @Inject + @Named("kickMessageAttribute") + private AttributeKey kickMessageAttribute; + @Inject @Named("playerAttribute") private AttributeKey playerAttribute; @Override public void onInject(Channel channel, boolean toServer) { - PacketBlocker blocker = new PacketBlocker(); - channel.pipeline().addBefore(packetDecoder, "floodgate_packet_blocker", blocker); - + // we have to add the packet blocker in the data handler, otherwise ProtocolSupport breaks channel.pipeline().addBefore( packetHandlerName, "floodgate_data_handler", - new SpigotDataHandler(config, handshakeHandler, blocker, logger) + new SpigotDataHandler(handshakeHandler, config, kickMessageAttribute) ); } diff --git a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java index ee99391a..ee6a2128 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java +++ b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java @@ -28,131 +28,109 @@ package org.geysermc.floodgate.addon.data; import static org.geysermc.floodgate.util.ReflectionUtils.getCastedValue; import static org.geysermc.floodgate.util.ReflectionUtils.setValue; -import com.google.common.collect.Queues; import com.mojang.authlib.GameProfile; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.Channel; +import io.netty.util.AttributeKey; import java.net.InetSocketAddress; -import java.util.Queue; -import lombok.RequiredArgsConstructor; -import org.geysermc.floodgate.api.handshake.HandshakeData; -import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.player.FloodgateHandshakeHandler; -import org.geysermc.floodgate.util.BedrockData; +import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult; import org.geysermc.floodgate.util.ClassNames; -import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.ProxyUtils; -@RequiredArgsConstructor -public final class SpigotDataHandler extends ChannelInboundHandlerAdapter { - private final FloodgateConfig config; - private final FloodgateHandshakeHandler handshakeHandler; - private final PacketBlocker blocker; - private final FloodgateLogger logger; - - private final Queue packetQueue = Queues.newConcurrentLinkedQueue(); - +public final class SpigotDataHandler extends CommonDataHandler { private Object networkManager; private FloodgatePlayer player; - @Override - public void channelRead(ChannelHandlerContext ctx, Object packet) throws Exception { - // prevent other packets from being handled while we handle the handshake packet - if (!packetQueue.isEmpty()) { - packetQueue.add(packet); - return; - } - - if (ClassNames.HANDSHAKE_PACKET.isInstance(packet)) { - blocker.enable(); - packetQueue.add(packet); - - networkManager = ctx.channel().pipeline().get("packet_handler"); - String handshakeValue = getCastedValue(packet, ClassNames.HANDSHAKE_HOST); - - handshakeHandler.handle(ctx.channel(), handshakeValue).thenApply(result -> { - HandshakeData handshakeData = result.getHandshakeData(); - - setValue(packet, ClassNames.HANDSHAKE_HOST, handshakeData.getHostname()); - - InetSocketAddress newIp = result.getNewIp(ctx.channel()); - if (newIp != null) { - setValue(networkManager, ClassNames.SOCKET_ADDRESS, newIp); - //todo the socket address will be overridden when bungeeData is true - } - - if (handshakeData.getDisconnectReason() != null) { - ctx.close(); //todo disconnect with message - return true; - } - - //todo use kickMessageAttribute and let this be common logic - - switch (result.getResultType()) { - case SUCCESS: - break; - case EXCEPTION: - logger.info(Constants.INTERNAL_ERROR_MESSAGE); - ctx.close(); - return true; - case DECRYPT_ERROR: - logger.info(config.getDisconnect().getInvalidKey()); - ctx.close(); - return true; - case INVALID_DATA_LENGTH: - int dataLength = result.getBedrockData().getDataLength(); - logger.info( - config.getDisconnect().getInvalidArgumentsLength(), - BedrockData.EXPECTED_LENGTH, dataLength - ); - ctx.close(); - return true; - default: // only continue when SUCCESS - return true; - } - - player = result.getFloodgatePlayer(); - boolean bungeeData = ProxyUtils.isProxyData(); - - if (!bungeeData) { - // Use a spoofedUUID for initUUID (just like Bungeecord) - setValue(networkManager, "spoofedUUID", player.getCorrectUniqueId()); - } - return bungeeData || player == null; - }).thenAccept(shouldRemove -> { - Object queuedPacket; - while ((queuedPacket = packetQueue.poll()) != null) { - try { - if (checkLogin(ctx, packet)) { - break; - } - } catch (Exception ignored) {} - ctx.fireChannelRead(queuedPacket); - } - - if (shouldRemove) { - ctx.pipeline().remove(SpigotDataHandler.this); - } - blocker.disable(); - }); - return; - } - - if (!checkLogin(ctx, packet)) { - ctx.fireChannelRead(packet); - } + public SpigotDataHandler( + FloodgateHandshakeHandler handshakeHandler, + FloodgateConfig config, + AttributeKey kickMessageAttribute) { + super(handshakeHandler, config, kickMessageAttribute, new PacketBlocker()); } - private boolean checkLogin(ChannelHandlerContext ctx, Object packet) throws Exception { + @Override + protected void setNewIp(Channel channel, InetSocketAddress newIp) { + //todo the socket address will be overridden when bungeeData is true + setValue(networkManager, ClassNames.SOCKET_ADDRESS, newIp); + } + + @Override + protected void setHostname(Object handshakePacket, String hostname) { + setValue(handshakePacket, ClassNames.HANDSHAKE_HOST, hostname); + } + + @Override + protected boolean shouldRemoveHandler(HandshakeResult result) { + player = result.getFloodgatePlayer(); + + if (getKickMessage() != null) { + // we also have to keep this handler if we want to kick then with a disconnect message + return false; + } else if (player == null) { + // player is not a Floodgate player + return true; + } + + // the server will do all the work if BungeeCord mode is enabled, + // otherwise we have to help the server. + boolean bungeeData = ProxyUtils.isProxyData(); + + if (!bungeeData) { + // Use a spoofedUUID for initUUID (just like Bungeecord) + setValue(networkManager, "spoofedUUID", player.getCorrectUniqueId()); + } + + return bungeeData; + } + + @Override + protected boolean shouldCallFireRead(Object queuedPacket) { + // we have to ignore the 'login start' packet if BungeeCord mode is disabled, + // otherwise the server might ask the user to login + try { + if (checkAndHandleLogin(queuedPacket)) { + return false; + } + } catch (Exception exception) { + exception.printStackTrace(); + } + return true; + } + + @Override + public boolean channelRead(Object packet) throws Exception { + if (ClassNames.HANDSHAKE_PACKET.isInstance(packet)) { + // ProtocolSupport would break if we added this during the creation of this handler + ctx.pipeline().addAfter("splitter", "floodgate_packet_blocker", blocker); + + networkManager = ctx.channel().pipeline().get("packet_handler"); + + handle(packet, getCastedValue(packet, ClassNames.HANDSHAKE_HOST)); + // otherwise, it'll get read twice. once by the packet queue and once by this method + return false; + } + + return !checkAndHandleLogin(packet); + } + + private boolean checkAndHandleLogin(Object packet) throws Exception { if (ClassNames.LOGIN_START_PACKET.isInstance(packet)) { - // we have to fake the offline player (login) cycle - Object loginListener = ClassNames.PACKET_LISTENER.get(networkManager); + Object packetListener = ClassNames.PACKET_LISTENER.get(networkManager); + + String kickMessage = getKickMessage(); + if (kickMessage != null) { + disconnect(packetListener, kickMessage); + return true; + } // check if the server is actually in the Login state - if (!ClassNames.LOGIN_LISTENER.isInstance(loginListener)) { + if (!ClassNames.LOGIN_LISTENER.isInstance(packetListener)) { // player is not in the login state, abort + + // I would've liked to close the channel for security reasons, but our big friend + // ProtocolSupport, who likes to break things, doesn't work otherwise ctx.pipeline().remove(this); return true; } @@ -161,8 +139,9 @@ public final class SpigotDataHandler extends ChannelInboundHandlerAdapter { GameProfile gameProfile = new GameProfile( player.getCorrectUniqueId(), player.getCorrectUsername() ); - setValue(loginListener, ClassNames.LOGIN_PROFILE, gameProfile); + setValue(packetListener, ClassNames.LOGIN_PROFILE, gameProfile); + // we have to fake the offline player (login) cycle // just like on Spigot: // LoginListener#initUUID @@ -170,10 +149,10 @@ public final class SpigotDataHandler extends ChannelInboundHandlerAdapter { // and the tick of LoginListener will do the rest - ClassNames.INIT_UUID.invoke(loginListener); + ClassNames.INIT_UUID.invoke(packetListener); Object loginHandler = - ClassNames.LOGIN_HANDLER_CONSTRUCTOR.newInstance(loginListener); + ClassNames.LOGIN_HANDLER_CONSTRUCTOR.newInstance(packetListener); ClassNames.FIRE_LOGIN_EVENTS.invoke(loginHandler); ctx.pipeline().remove(this); @@ -182,11 +161,16 @@ public final class SpigotDataHandler extends ChannelInboundHandlerAdapter { return false; } - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - if (config.isDebug()) { - cause.printStackTrace(); + private void disconnect(Object packetListener, String kickMessage) throws Exception { + // both versions close the channel for us + if (ClassNames.LOGIN_LISTENER.isInstance(packetListener)) { + ClassNames.LOGIN_DISCONNECT.invoke(packetListener, kickMessage); + } else { + // ProtocolSupport for example has their own PacketLoginInListener implementation + ClassNames.NETWORK_EXCEPTION_CAUGHT.invoke( + networkManager, + ctx, new IllegalStateException(kickMessage) + ); } } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java b/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java index 3f6e11a9..4f6036ac 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java @@ -33,6 +33,7 @@ import static org.geysermc.floodgate.util.ReflectionUtils.makeAccessible; import com.google.common.base.Preconditions; import com.mojang.authlib.GameProfile; +import io.netty.channel.ChannelHandlerContext; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -65,6 +66,8 @@ public class ClassNames { public static final Method IS_WHITELISTED; public static final Method ADD_WHITELIST_ENTRY; public static final Method REMOVE_WHITELIST_ENTRY; + public static final Method LOGIN_DISCONNECT; + public static final Method NETWORK_EXCEPTION_CAUGHT; public static final Method INIT_UUID; public static final Method FIRE_LOGIN_EVENTS; @@ -166,6 +169,15 @@ public class ClassNames { LOGIN_PROFILE = getFieldOfType(LOGIN_LISTENER, GameProfile.class); checkNotNull(LOGIN_PROFILE, "Profile from LoginListener"); + LOGIN_DISCONNECT = getMethod(LOGIN_LISTENER, "disconnect", String.class); + checkNotNull(LOGIN_DISCONNECT, "LoginListener's disconnect method"); + + NETWORK_EXCEPTION_CAUGHT = getMethod( + networkManager, + "exceptionCaught", + ChannelHandlerContext.class, Throwable.class + ); + // there are multiple no-arg void methods INIT_UUID = getMethod(LOGIN_LISTENER, "initUUID"); checkNotNull(INIT_UUID, "initUUID from LoginListener"); diff --git a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java index afa3feae..b185507c 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java +++ b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java @@ -31,24 +31,18 @@ import static org.geysermc.floodgate.util.ReflectionUtils.getField; import static org.geysermc.floodgate.util.ReflectionUtils.getPrefixedClass; import static org.geysermc.floodgate.util.ReflectionUtils.setValue; -import com.google.common.collect.Queues; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.Field; import java.net.InetSocketAddress; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import lombok.RequiredArgsConstructor; -import org.geysermc.floodgate.api.handshake.HandshakeData; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; -import org.geysermc.floodgate.config.ProxyFloodgateConfig; +import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.player.FloodgateHandshakeHandler; -import org.geysermc.floodgate.util.Constants; +import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult; +import org.geysermc.floodgate.player.FloodgateHandshakeHandler.ResultType; -@RequiredArgsConstructor -public final class VelocityProxyDataHandler extends ChannelInboundHandlerAdapter { +public final class VelocityProxyDataHandler extends CommonDataHandler { private static final Field HANDSHAKE; private static final Class HANDSHAKE_PACKET; private static final Field HANDSHAKE_SERVER_ADDRESS; @@ -71,97 +65,47 @@ public final class VelocityProxyDataHandler extends ChannelInboundHandlerAdapter REMOTE_ADDRESS = getField(minecraftConnection, "remoteAddress"); } - private final ProxyFloodgateConfig config; - private final FloodgateHandshakeHandler handshakeHandler; - private final PacketBlocker blocker; - private final AttributeKey kickMessageAttribute; private final FloodgateLogger logger; - private final Queue packetQueue = Queues.newConcurrentLinkedQueue(); - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - // prevent other packets from being handled while we handle the handshake packet - if (!packetQueue.isEmpty()) { - packetQueue.add(msg); - return; - } - - // we're only interested in the Handshake packet. - // it should be the first packet but you never know - if (HANDSHAKE_PACKET.isInstance(msg)) { - blocker.enable(); - packetQueue.add(msg); - - handleClientToProxy(ctx, msg).thenRun(() -> { - Object packet; - while ((packet = packetQueue.poll()) != null) { - ctx.fireChannelRead(packet); - } - ctx.pipeline().remove(this); - blocker.disable(); - }); - return; - } - - ctx.fireChannelRead(msg); + public VelocityProxyDataHandler( + FloodgateConfig config, + FloodgateHandshakeHandler handshakeHandler, + PacketBlocker blocker, + AttributeKey kickMessageAttribute, + FloodgateLogger logger) { + super(handshakeHandler, config, kickMessageAttribute, blocker); + this.logger = logger; } - private CompletableFuture handleClientToProxy(ChannelHandlerContext ctx, Object packet) { - String address = getCastedValue(packet, HANDSHAKE_SERVER_ADDRESS); + @Override + protected void setNewIp(Channel channel, InetSocketAddress newIp) { + setValue(channel.pipeline().get("handler"), REMOTE_ADDRESS, newIp); + } - return handshakeHandler.handle(ctx.channel(), address).thenAccept(result -> { - HandshakeData handshakeData = result.getHandshakeData(); - - InetSocketAddress newIp = result.getNewIp(ctx.channel()); - if (newIp != null) { - Object connection = ctx.pipeline().get("handler"); - setValue(connection, REMOTE_ADDRESS, newIp); - } - - setValue(packet, HANDSHAKE_SERVER_ADDRESS, handshakeData.getHostname()); - - if (handshakeData.getDisconnectReason() != null) { - ctx.channel().attr(kickMessageAttribute).set(handshakeData.getDisconnectReason()); - return; - } - - switch (result.getResultType()) { - case SUCCESS: - break; - case EXCEPTION: - ctx.channel().attr(kickMessageAttribute) - .set(Constants.INTERNAL_ERROR_MESSAGE); - return; - case DECRYPT_ERROR: - ctx.channel().attr(kickMessageAttribute) - .set(config.getDisconnect().getInvalidKey()); - return; - case INVALID_DATA_LENGTH: - ctx.channel().attr(kickMessageAttribute) - .set(config.getDisconnect().getInvalidArgumentsLength()); - return; - default: // only continue when SUCCESS - return; - } + @Override + protected void setHostname(Object handshakePacket, String hostname) { + setValue(handshakePacket, HANDSHAKE_SERVER_ADDRESS, hostname); + } + @Override + protected boolean shouldRemoveHandler(HandshakeResult result) { + if (result.getResultType() == ResultType.SUCCESS) { FloodgatePlayer player = result.getFloodgatePlayer(); - logger.info("Floodgate player who is logged in as {} {} joined", player.getCorrectUsername(), player.getCorrectUniqueId()); - }).handle((v, error) -> { - if (error != null) { - error.printStackTrace(); - } - return v; - }); + } + return true; } @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - if (config.isDebug()) { - cause.printStackTrace(); + public boolean channelRead(Object packet) { + // we're only interested in the Handshake packet. + // it should be the first packet but you never know + if (HANDSHAKE_PACKET.isInstance(packet)) { + handle(packet, getCastedValue(packet, HANDSHAKE_SERVER_ADDRESS)); + // otherwise, it'll get read twice. once by the packet queue and once by this method + return false; } + return true; } }