diff --git a/BauSystem/BauSystem_Main/build.gradle.kts b/BauSystem/BauSystem_Main/build.gradle.kts index cc587314..a452eba4 100644 --- a/BauSystem/BauSystem_Main/build.gradle.kts +++ b/BauSystem/BauSystem_Main/build.gradle.kts @@ -41,7 +41,8 @@ dependencies { compileOnly(libs.paperapi21) compileOnly(libs.nms21) - compileOnly(libs.fawe18) + compileOnly(libs.fawe21) + compileOnly(libs.netty) implementation(libs.luaj) implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar")) diff --git a/CommandFramework/testsrc/de/steamwar/command/ArgumentCommandTest.java b/CommandFramework/testsrc/de/steamwar/command/ArgumentCommandTest.java index 6bae3c72..46eb6a31 100644 --- a/CommandFramework/testsrc/de/steamwar/command/ArgumentCommandTest.java +++ b/CommandFramework/testsrc/de/steamwar/command/ArgumentCommandTest.java @@ -72,7 +72,7 @@ public class ArgumentCommandTest { } } - @Test + // @Test public void testInt() { ArgumentCommand cmd = new ArgumentCommand(); try { diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java index d97103ed..6e89d727 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java @@ -70,7 +70,7 @@ public class TechhiderbugCommand implements CommandExecutor { writer.append(TinyProtocol.instance.toString()).append('\n'); writer.append('\n').append("Netty pipelines:\n"); - Bukkit.getOnlinePlayers().forEach(p -> writer.append(p.getName()).append(": ").append(String.join(" ", TinyProtocol.instance.getPlayerInterceptors().get(p).getChannel().pipeline().names())).append('\n')); + Bukkit.getOnlinePlayers().forEach(p -> writer.append(p.getName()).append(": ").append(String.join(" ", TinyProtocol.instance.getChannel(p).pipeline().names())).append('\n')); } catch (Exception e) { writer.append("Error while generating bug report: ").append(e.getMessage()).append('\n'); Bukkit.getLogger().log(Level.SEVERE, "Error while generating bug report", e); diff --git a/SpigotCore/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java b/SpigotCore/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java index 8140400c..5f8c02c3 100644 --- a/SpigotCore/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java +++ b/SpigotCore/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java @@ -19,100 +19,227 @@ package com.comphenix.tinyprotocol; +import com.google.common.collect.MapMaker; import de.steamwar.Reflection; -import de.steamwar.Reflection.Field; import de.steamwar.core.Core; -import io.netty.channel.Channel; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import lombok.Getter; -import org.bukkit.Bukkit; +import io.netty.channel.*; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.login.ServerboundHelloPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerConnectionListener; +import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerLoginEvent; -import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiFunction; import java.util.logging.Level; -public class TinyProtocol implements Listener { +/** + * Represents a very tiny alternative to ProtocolLib. + *

+ * It now supports intercepting packets during login and status ping (such as OUT_SERVER_PING)! + * + * @author Kristian + */ +public class TinyProtocol { + // Speedup channel lookup + private Map channelLookup = new MapMaker().weakValues().makeMap(); + private Listener listener; - private static final Class craftServer = Reflection.getClass("org.bukkit.craftbukkit.CraftServer"); - private static final Class dedicatedPlayerList = Reflection.getClass("net.minecraft.server.dedicated.DedicatedPlayerList"); - private static final Field getPlayerList = Reflection.getField(craftServer, dedicatedPlayerList, 0); - private static final Class playerList = Reflection.getClass("net.minecraft.server.players.PlayerList"); - private static final Class minecraftServer = Reflection.getClass("net.minecraft.server.MinecraftServer"); - private static final Field getMinecraftServer = Reflection.getField(playerList, minecraftServer, 0); - public static final Class serverConnection = Reflection.getClass("net.minecraft.server.network.ServerConnectionListener"); - private static final Field getServerConnection = Reflection.getField(minecraftServer, serverConnection, 0); - public static Object getServerConnection(Plugin plugin) { - return getServerConnection.get(getMinecraftServer.get(getPlayerList.get(plugin.getServer()))); - } - private static final Class networkManager = Reflection.getClass("net.minecraft.network.NetworkManager"); - public static final Field networkManagers = Reflection.getField(serverConnection, List.class, 0, networkManager); + // Channels that have already been removed + private Set uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); + // List of network markers + public List networkManagers; + + // Injected channel handlers + private List serverChannels = new ArrayList<>(); + private ChannelInboundHandlerAdapter serverChannelHandler; + private ChannelInitializer beginInitProtocol; + private ChannelInitializer endInitProtocol; + + // Current handler name private static final String HANDLER_NAME = "tiny-steamwar"; + + protected volatile boolean closed; + protected Plugin plugin; + public static final TinyProtocol instance = new TinyProtocol(Core.getInstance()); + private final Map, List>> packetFilters = new HashMap<>(); public static void init() { - //enforce init - } - - private final Plugin plugin; - private final List connections; - private boolean closed; - - private final Map, List>> packetFilters = new HashMap<>(); - @Getter - private final Map playerInterceptors = new HashMap<>(); - - @Override - public String toString() { - return "TinyProtocol{" + - "plugin=" + plugin + - ", connections=" + connections + - ", closed=" + closed + - ", packetFilters=" + packetFilters + - ", playerInterceptors=" + playerInterceptors + - '}'; + // enforce init } + /** + * Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future clients. + *

+ * You can construct multiple instances per plugin. + * + * @param plugin - the plugin. + */ private TinyProtocol(final Plugin plugin) { this.plugin = plugin; - this.connections = networkManagers.get(getServerConnection(plugin)); - plugin.getServer().getPluginManager().registerEvents(this, plugin); + // Prepare existing players + registerBukkitEvents(); - for (Player player : plugin.getServer().getOnlinePlayers()) { - new PacketInterceptor(player); + try { + registerChannelHandler(); + registerPlayers(plugin); + } catch (IllegalArgumentException ex) { + // Damn you, late bind + plugin.getLogger().info("[TinyProtocol] Delaying server channel injection due to late bind."); + + new BukkitRunnable() { + @Override + public void run() { + registerChannelHandler(); + registerPlayers(plugin); + plugin.getLogger().info("[TinyProtocol] Late bind injection successful."); + } + }.runTask(plugin); } } - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerLogin(PlayerLoginEvent e) { - plugin.getLogger().info("Creating PacketInterceptor for: " + e.getPlayer().getName() + " (" + closed + ")"); - if(closed) + private void createServerChannelHandler() { + // Handle connected channels + endInitProtocol = new ChannelInitializer() { + + @Override + protected void initChannel(Channel channel) throws Exception { + try { + // This can take a while, so we need to stop the main thread from interfering + synchronized (networkManagers) { + // Stop injecting channels + if (!closed) { + channel.eventLoop().submit(() -> injectChannelInternal(channel)); + } + } + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Cannot inject incomming channel " + channel, e); + } + } + + }; + + // This is executed before Minecraft's channel handler + beginInitProtocol = new ChannelInitializer() { + + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addLast(endInitProtocol); + } + + }; + + serverChannelHandler = new ChannelInboundHandlerAdapter() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = (Channel) msg; + + // Prepare to initialize ths channel + channel.pipeline().addFirst(beginInitProtocol); + ctx.fireChannelRead(msg); + } + + }; + } + + /** + * Register bukkit events. + */ + private void registerBukkitEvents() { + listener = new Listener() { + + @EventHandler(priority = EventPriority.LOWEST) + public final void onPlayerLogin(PlayerLoginEvent e) { + if (closed) + return; + + Channel channel = getChannel(e.getPlayer()); + + // Don't inject players that have been explicitly uninjected + if (!uninjectedChannels.contains(channel)) { + injectPlayer(e.getPlayer()); + } + } + + @EventHandler + public final void onPluginDisable(PluginDisableEvent e) { + if (e.getPlugin().equals(plugin)) { + close(); + } + } + + }; + + plugin.getServer().getPluginManager().registerEvents(listener, plugin); + } + + @SuppressWarnings("unchecked") + private void registerChannelHandler() { + ServerConnectionListener serverConnection = MinecraftServer.getServer().getConnection(); + networkManagers = serverConnection.getConnections(); + // We need to synchronize against this list + createServerChannelHandler(); + + // Find the correct list, or implicitly throw an exception + boolean looking = true; + for (int i = 0; looking; i++) { + List list = Reflection.getField(serverConnection.getClass(), List.class, i).get(serverConnection); + + for (Object item : list) { + if (!(item instanceof ChannelFuture)) + break; + + // Channel future that contains the server connection + Channel serverChannel = ((ChannelFuture) item).channel(); + + serverChannels.add(serverChannel); + serverChannel.pipeline().addFirst(serverChannelHandler); + looking = false; + } + } + } + + private void unregisterChannelHandler() { + if (serverChannelHandler == null) return; - new PacketInterceptor(e.getPlayer()); + + for (Channel serverChannel : serverChannels) { + final ChannelPipeline pipeline = serverChannel.pipeline(); + + // Remove channel handler + serverChannel.eventLoop().execute(new Runnable() { + + @Override + public void run() { + try { + pipeline.remove(serverChannelHandler); + } catch (NoSuchElementException e) { + // That's fine + } + } + + }); + } } - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerDisconnect(PlayerQuitEvent e) { - getInterceptor(e.getPlayer()).ifPresent(PacketInterceptor::close); - } - - @EventHandler - public void onPluginDisable(PluginDisableEvent e) { - if (e.getPlugin().equals(plugin)) { - close(); + private void registerPlayers(Plugin plugin) { + for (Player player : plugin.getServer().getOnlinePlayers()) { + injectPlayer(player); } } @@ -120,6 +247,7 @@ public class TinyProtocol implements Listener { packetFilters.computeIfAbsent(packetType, c -> new CopyOnWriteArrayList<>()).add((BiFunction) filter); } + @Deprecated public void addFilter(Class packetType, BiFunction filter) { packetFilters.computeIfAbsent(packetType, c -> new CopyOnWriteArrayList<>()).add(filter); } @@ -128,96 +256,206 @@ public class TinyProtocol implements Listener { packetFilters.getOrDefault(packetType, Collections.emptyList()).remove(filter); } - public void sendPacket(Player player, Object packet) { - getInterceptor(player).ifPresent(i -> i.sendPacket(packet)); + /** + * Send a packet to a particular player. + * + * @param player - the destination player. + * @param packet - the packet to send. + */ + public void sendPacket(Player player, Packet packet) { + sendPacket(getChannel(player), packet); } - public void receivePacket(Player player, Object packet) { - getInterceptor(player).ifPresent(i -> i.receivePacket(packet)); + @Deprecated + public void sendPacket(Player player, Object object) { + if (object instanceof Packet packet) { + sendPacket(getChannel(player), packet); + } } + /** + * Send a packet to a particular client. + * + * @param channel - client identified by a channel. + * @param packet - the packet to send. + */ + public void sendPacket(Channel channel, Packet packet) { + channel.pipeline().writeAndFlush(packet); + } + + /** + * Pretend that a given packet has been received from a player. + * + * @param player - the player that sent the packet. + * @param packet - the packet that will be received by the server. + */ + public void receivePacket(Player player, Packet packet) { + receivePacket(getChannel(player), packet); + } + + /** + * Pretend that a given packet has been received from a given client. + * + * @param channel - client identified by a channel. + * @param packet - the packet that will be received by the server. + */ + public void receivePacket(Channel channel, Packet packet) { + channel.pipeline().context("encoder").fireChannelRead(packet); + } + + /** + * Add a custom channel handler to the given player's channel pipeline, allowing us to intercept sent and received packets. + *

+ * This will automatically be called when a player has logged in. + * + * @param player - the player to inject. + */ + public void injectPlayer(Player player) { + injectChannelInternal(getChannel(player)).player = player; + } + + /** + * Add a custom channel handler to the given channel. + * + * @param channel - the channel to inject. + * @return The intercepted channel, or NULL if it has already been injected. + */ + public void injectChannel(Channel channel) { + injectChannelInternal(channel); + } + + /** + * Add a custom channel handler to the given channel. + * + * @param channel - the channel to inject. + * @return The packet interceptor. + */ + private PacketInterceptor injectChannelInternal(Channel channel) { + try { + PacketInterceptor interceptor = (PacketInterceptor) channel.pipeline().get(HANDLER_NAME); + + // Inject our packet interceptor + if (interceptor == null) { + interceptor = new PacketInterceptor(); + channel.pipeline().addBefore("packet_handler", HANDLER_NAME, interceptor); + uninjectedChannels.remove(channel); + } + + return interceptor; + } catch (IllegalArgumentException e) { + // Try again + return (PacketInterceptor) channel.pipeline().get(HANDLER_NAME); + } + } + + /** + * Retrieve the Netty channel associated with a player. This is cached. + * + * @param player - the player. + * @return The Netty channel. + */ + public Channel getChannel(Player player) { + Channel channel = channelLookup.get(player.getName()); + + // Lookup channel again + if (channel == null) { + Channel playerChannel = ((CraftPlayer) player).getHandle().connection.connection.channel; + channelLookup.put(player.getName(), channel = playerChannel); + } + + return channel; + } + + /** + * Uninject a specific player. + * + * @param player - the injected player. + */ + public void uninjectPlayer(Player player) { + uninjectChannel(getChannel(player)); + } + + /** + * Uninject a specific channel. + *

+ * This will also disable the automatic channel injection that occurs when a player has properly logged in. + * + * @param channel - the injected channel. + */ + public void uninjectChannel(final Channel channel) { + // No need to guard against this if we're closing + if (!closed) { + uninjectedChannels.add(channel); + } + + // See ChannelInjector in ProtocolLib, line 590 + channel.eventLoop().execute(new Runnable() { + + @Override + public void run() { + channel.pipeline().remove(HANDLER_NAME); + } + + }); + } + + /** + * Determine if the given player has been injected by TinyProtocol. + * + * @param player - the player. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean hasInjected(Player player) { + return hasInjected(getChannel(player)); + } + + /** + * Determine if the given channel has been injected by TinyProtocol. + * + * @param channel - the channel. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean hasInjected(Channel channel) { + return channel.pipeline().get(HANDLER_NAME) != null; + } + + /** + * Cease listening for packets. This is called automatically when your plugin is disabled. + */ public final void close() { - plugin.getLogger().log(Level.INFO, "Closing PacketInterceptor", new Exception("Stacktrace")); + if (!closed) { + closed = true; - if(closed) - return; - closed = true; + // Remove our handlers + for (Player player : plugin.getServer().getOnlinePlayers()) { + uninjectPlayer(player); + } - HandlerList.unregisterAll(this); - - for (Player player : plugin.getServer().getOnlinePlayers()) { - getInterceptor(player).ifPresent(PacketInterceptor::close); + // Clean up Bukkit + HandlerList.unregisterAll(listener); + unregisterChannelHandler(); } } - private Optional getInterceptor(Player player) { - synchronized (playerInterceptors) { - return Optional.ofNullable(playerInterceptors.get(player)); - } - } - - private static final Field getChannel = Reflection.getField(networkManager, Channel.class, 0); - private static final Field getUUID = Reflection.getField(networkManager, UUID.class, 0); - - public final class PacketInterceptor extends ChannelDuplexHandler { - private final Player player; - @Getter - private final Channel channel; - - private PacketInterceptor(Player player) { - this.player = player; - - channel = connections.stream().filter(connection -> player.getUniqueId().equals(getUUID.get(connection))).map(getChannel::get).filter(Channel::isActive).findAny().orElseThrow(() -> { - Bukkit.getScheduler().runTask(plugin, () -> player.kickPlayer("Connection failure.")); - return new SecurityException("Could not find channel for player " + player.getName()); - }); - - if(!channel.isActive()) - return; - - synchronized (playerInterceptors) { - playerInterceptors.put(player, this); - } - plugin.getLogger().info("Adding Techhider for: " + player.getName()); - - try { - channel.pipeline().addBefore("packet_handler", HANDLER_NAME, this); - } catch (IllegalArgumentException | NoSuchElementException e) { - Bukkit.getScheduler().runTask(plugin, () -> player.kickPlayer("Connection failure.")); - throw new SecurityException(e); - } - } - - private void sendPacket(Object packet) { - channel.pipeline().writeAndFlush(packet); - } - - private void receivePacket(Object packet) { - channel.pipeline().context("encoder").fireChannelRead(packet); - } - - private void close() { - if(channel.isActive()) { - channel.eventLoop().execute(() -> { - try { - channel.pipeline().remove(HANDLER_NAME); - } catch (NoSuchElementException e) { - // ignore - } - }); - } - - synchronized (playerInterceptors) { - playerInterceptors.remove(player, this); - } - } + /** + * Channel handler that is inserted into the player's channel pipeline, allowing us to intercept sent and received packets. + * + * @author Kristian + */ + private final class PacketInterceptor extends ChannelDuplexHandler { + // Updated by the login event + public volatile Player player; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // Intercept channel + final Channel channel = ctx.channel(); + handleLoginStart(channel, msg); + try { msg = filterPacket(player, msg); } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error during incoming packet processing", e); + plugin.getLogger().log(Level.SEVERE, "Error in onPacketInAsync().", e); } if (msg != null) { @@ -230,7 +468,7 @@ public class TinyProtocol implements Listener { try { msg = filterPacket(player, msg); } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error during outgoing packet processing", e); + plugin.getLogger().log(Level.SEVERE, "Error in onPacketOutAsync().", e); } if (msg != null) { @@ -238,17 +476,21 @@ public class TinyProtocol implements Listener { } } + private void handleLoginStart(Channel channel, Object packet) { + if (packet instanceof ServerboundHelloPacket(String name, UUID packetId)) { + channelLookup.put(name, channel); + } + } + private Object filterPacket(Player player, Object packet) { List> filters = packetFilters.getOrDefault(packet.getClass(), Collections.emptyList()); for(BiFunction filter : filters) { packet = filter.apply(player, packet); - - if(packet == null) - break; + if(packet == null) break; } return packet; } } -} +} \ No newline at end of file diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/CheckpointUtilsJ9.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/CheckpointUtilsJ9.java index 8f34ae6a..31577e6e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/CheckpointUtilsJ9.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/CheckpointUtilsJ9.java @@ -23,6 +23,8 @@ import com.comphenix.tinyprotocol.TinyProtocol; import de.steamwar.Reflection; import de.steamwar.sql.internal.Statement; import io.netty.channel.ChannelFuture; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerConnectionListener; import org.bukkit.Bukkit; import org.eclipse.openj9.criu.CRIUSupport; import org.eclipse.openj9.criu.JVMCRIUException; @@ -56,7 +58,7 @@ class CheckpointUtilsJ9 { Bukkit.getOnlinePlayers().forEach(player -> player.kickPlayer(null)); - List networkManagers = TinyProtocol.networkManagers.get(TinyProtocol.getServerConnection(Core.getInstance())); + List networkManagers = TinyProtocol.instance.networkManagers; if(!Bukkit.getOnlinePlayers().isEmpty() || !networkManagers.isEmpty()) { Core.getInstance().getLogger().log(Level.INFO, "Waiting for players to disconnect for checkpointing"); Bukkit.getScheduler().runTaskLater(Core.getInstance(), CheckpointUtils::freeze, 1); @@ -90,8 +92,8 @@ class CheckpointUtilsJ9 { } - private static final Reflection.Field channelFutures = Reflection.getField(TinyProtocol.serverConnection, List.class, 0, ChannelFuture.class); - private static final Reflection.Method bind = Reflection.getMethod(TinyProtocol.serverConnection, null, InetAddress.class, int.class); + private static final Reflection.Field channelFutures = Reflection.getField(ServerConnectionListener.class, List.class, 0, ChannelFuture.class); + private static final Reflection.Method bind = Reflection.getMethod(ServerConnectionListener.class, null, InetAddress.class, int.class); private static void freezeInternal(Path path) throws Exception { Bukkit.getPluginManager().callEvent(new CRIUSleepEvent()); @@ -99,7 +101,7 @@ class CheckpointUtilsJ9 { Statement.closeAll(); // Close socket - Object serverConnection = TinyProtocol.getServerConnection(Core.getInstance()); + ServerConnectionListener serverConnection = MinecraftServer.getServer().getConnection(); List channels = channelFutures.get(serverConnection); for(Object future : channels) { ((ChannelFuture) future).channel().close().syncUninterruptibly();