Last active
January 25, 2023 16:49
-
-
Save BlackOfWorld/7cad88c33bbdc1cfc2a0c8575a369fc8 to your computer and use it in GitHub Desktop.
Revisions
-
BlackOfWorld revised this gist
Jan 25, 2023 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -13,7 +13,7 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; @@ -162,7 +162,7 @@ private void registerBukkitEvents() { listener = new Listener() { @EventHandler(priority = EventPriority.LOWEST) public final void onPlayerJoin(PlayerJoinEvent e) throws InvocationTargetException, IllegalAccessException { if (closed) return; -
BlackOfWorld revised this gist
Jan 24, 2023 . 3 changed files with 59 additions and 34 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,13 @@ public class BukkitReflection { public static ServerPlayer getServerPlayer(Player p) { var m = Reflection.getMethodCached("{obc}.entity.CraftPlayer", "getHandle"); return invoke(m, p); } private static <T> T invoke(java.lang.reflect.Method m, Object instance, Object... args) { try { return (T) m.invoke(instance, args); } catch (Exception e) { throw new RuntimeException(e); } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,14 @@ import java.lang.reflect.*; import java.security.AccessController; import java.security.PrivilegedAction; // Use Kristian's (Original author of TinyProtocol) Reflection class, it's good and has everything you'll need public class Reflection { public static < T extends AccessibleObject > T setAccessible(T object, boolean access) { AccessController.doPrivileged((PrivilegedAction)() - > { object.setAccessible(access); return null; }); return object; } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,3 @@ import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import com.mojang.authlib.GameProfile; @@ -42,33 +41,33 @@ public abstract class TinyProtocol { protected volatile boolean closed; protected Plugin plugin; // Speedup channel lookup private Map < String, Channel > channelLookup = new MapMaker().weakValues().makeMap(); private Listener listener; // Channels that have already been removed private Set < Channel > uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); // List of network markers private List < Object > networkManagers; // Injected channel handlers private List < Channel > serverChannels = Lists.newArrayList(); private ChannelInboundHandlerAdapter serverChannelHandler; private ChannelInitializer < Channel > beginInitProtocol; private ChannelInitializer < Channel > endInitProtocol; // Current handler name private String handlerName; static { var fields = ServerConnectionListener.class.getDeclaredFields(); for (var field: fields) { var mod = field.getModifiers(); if (Modifier.isFinal(mod) && Modifier.isPrivate(mod) && field.getType().equals(List.class)) { getChannels = field; Reflection.setAccessible(getChannels, true); continue; } else if (getChannels != null && Modifier.isFinal(mod) && field.getType().equals(List.class)) { getNetworkMarkers = field; Reflection.setAccessible(getNetworkMarkers, true); break; } } } /** @@ -116,26 +115,25 @@ public void run() { private void createServerChannelHandler() { // Handle connected channels endInitProtocol = new ChannelInitializer < Channel > () { @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) {} } }; // This is executed before Minecraft's channel handler beginInitProtocol = new ChannelInitializer < Channel > () { @Override protected void initChannel(Channel channel) throws Exception { @@ -194,14 +192,14 @@ private void registerChannelHandler() throws InvocationTargetException, IllegalA boolean looking = true; // We need to synchronize against this list networkManagers = (List < Object > ) getNetworkMarkers.get(serverConnection); createServerChannelHandler(); // Find the correct list, or implicitly throw an exception for (int i = 0; looking; i++) { List < Object > list = (List < Object > ) getChannels.get(serverConnection); for (Object item: list) { if (!(item instanceof ChannelFuture)) break; @@ -219,7 +217,7 @@ private void unregisterChannelHandler() { if (serverChannelHandler == null) return; for (Channel serverChannel: serverChannels) { final ChannelPipeline pipeline = serverChannel.pipeline(); // Remove channel handler @@ -239,7 +237,7 @@ public void run() { } private void registerPlayers(Plugin plugin) throws InvocationTargetException, IllegalAccessException { for (Player player: plugin.getServer().getOnlinePlayers()) { injectPlayer(player); } } @@ -418,7 +416,7 @@ public void uninjectChannel(final Channel channel) { } // See ChannelInjector in ProtocolLib, line 590 channel.eventLoop().execute(() - > channel.pipeline().remove(handlerName)); } /** @@ -449,7 +447,7 @@ public final void close() throws InvocationTargetException, IllegalAccessExcepti closed = true; // Remove our handlers for (Player player: plugin.getServer().getOnlinePlayers()) { uninjectPlayer(player); } @@ -500,7 +498,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) private void handleLoginStart(Channel channel, Object packet) throws IllegalAccessException { if (ClientboundGameProfilePacket.class.isInstance(packet)) { GameProfile profile = ((ClientboundGameProfilePacket) packet).getGameProfile(); channelLookup.put(profile.getName(), channel); } } -
BlackOfWorld created this gist
Jan 24, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,508 @@ import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import com.mojang.authlib.GameProfile; import io.netty.channel.*; import net.blackofworld.sneakybastard.Utils.BukkitReflection; import net.blackofworld.sneakybastard.Utils.Reflection; import net.minecraft.network.protocol.login.ClientboundGameProfilePacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerConnectionListener; import net.minecraft.server.network.ServerGamePacketListenerImpl; 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.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; /** * Represents a very tiny alternative to ProtocolLib. * <p> * It now supports intercepting packets during login and status ping (such as OUT_SERVER_PING)! * * @author Kristian */ public abstract class TinyProtocol { private static final AtomicInteger ID = new AtomicInteger(0); private static Field getNetworkMarkers = null; private static Field getChannels = null; protected volatile boolean closed; protected Plugin plugin; // Speedup channel lookup private Map<String, Channel> channelLookup = new MapMaker().weakValues().makeMap(); private Listener listener; // Channels that have already been removed private Set<Channel> uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); // List of network markers private List<Object> networkManagers; // Injected channel handlers private List<Channel> serverChannels = Lists.newArrayList(); private ChannelInboundHandlerAdapter serverChannelHandler; private ChannelInitializer<Channel> beginInitProtocol; private ChannelInitializer<Channel> endInitProtocol; // Current handler name private String handlerName; static { var fields = ServerConnectionListener.class.getDeclaredFields(); for(var field : fields) { var mod = field.getModifiers(); if(Modifier.isFinal(mod) && Modifier.isPrivate(mod) && field.getType().equals(List.class)) { getChannels = field; Reflection.setAccessible(getChannels, true); continue; } else if(getChannels != null && Modifier.isFinal(mod) && field.getType().equals(List.class)) { getNetworkMarkers = field; Reflection.setAccessible(getNetworkMarkers, true); break; } } } /** * Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future clients. * <p> * You can construct multiple instances per plugin. * * @param plugin - the plugin. */ public TinyProtocol(final Plugin plugin) { this.plugin = plugin; // Compute handler name this.handlerName = getHandlerName(); // Prepare existing players registerBukkitEvents(); try { try { registerChannelHandler(); registerPlayers(plugin); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } catch (IllegalArgumentException ex) { // Damn you, late bind new BukkitRunnable() { @Override public void run() { try { registerChannelHandler(); registerPlayers(plugin); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }.runTask(plugin); } } private void createServerChannelHandler() { // Handle connected channels endInitProtocol = new ChannelInitializer<Channel>() { @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) { } } }; // This is executed before Minecraft's channel handler beginInitProtocol = new ChannelInitializer<Channel>() { @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 the channel channel.pipeline().addFirst(beginInitProtocol); //channel.pipeline().addFirst(customPacketHandler); // TODO: work on this ctx.fireChannelRead(msg); } }; } /** * Register bukkit events. */ private void registerBukkitEvents() { listener = new Listener() { @EventHandler(priority = EventPriority.LOWEST) public final void onPlayerLogin(PlayerLoginEvent e) throws InvocationTargetException, IllegalAccessException { 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) throws InvocationTargetException, IllegalAccessException { if (e.getPlugin().equals(plugin)) { close(); } } }; plugin.getServer().getPluginManager().registerEvents(listener, plugin); } @SuppressWarnings("unchecked") private void registerChannelHandler() throws InvocationTargetException, IllegalAccessException { ServerConnectionListener serverConnection = MinecraftServer.getServer().getConnection(); boolean looking = true; // We need to synchronize against this list networkManagers = (List<Object>) getNetworkMarkers.get(serverConnection); createServerChannelHandler(); // Find the correct list, or implicitly throw an exception for (int i = 0; looking; i++) { List<Object> list = (List<Object>) getChannels.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; 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 } } }); } } private void registerPlayers(Plugin plugin) throws InvocationTargetException, IllegalAccessException { for (Player player : plugin.getServer().getOnlinePlayers()) { injectPlayer(player); } } /** * Invoked when the server is starting to send a packet to a player. * <p> * Note that this is not executed on the main thread. * * @param receiver - the receiving player, NULL for early login/status packets. * @param channel - the channel that received the packet. Never NULL. * @param packet - the packet being sent. * @return The packet to send instead, or NULL to cancel the transmission. */ public Object onPacketOutAsync(Player receiver, Channel channel, Object packet) { return packet; } /** * Invoked when the server has received a packet from a given player. * <p> * Use {@link Channel#remoteAddress()} to get the remote address of the client. * * @param sender - the player that sent the packet, NULL for early login/status packets. * @param channel - channel that received the packet. Never NULL. * @param packet - the packet being received. * @return The packet to recieve instead, or NULL to cancel. */ public Object onPacketInAsync(Player sender, Channel channel, Object packet) { return packet; } /** * Send a packet to a particular player. * <p> * Note that {@link #onPacketOutAsync(Player, Channel, Object)} will be invoked with this packet. * * @param player - the destination player. * @param packet - the packet to send. */ public void sendPacket(Player player, Object packet) throws InvocationTargetException, IllegalAccessException { sendPacket(getChannel(player), packet); } /** * Send a packet to a particular client. * <p> * Note that {@link #onPacketOutAsync(Player, Channel, Object)} will be invoked with this packet. * * @param channel - client identified by a channel. * @param packet - the packet to send. */ public void sendPacket(Channel channel, Object packet) { channel.pipeline().writeAndFlush(packet); } /** * Pretend that a given packet has been received from a player. * <p> * Note that {@link #onPacketInAsync(Player, Channel, Object)} will be invoked with this packet. * * @param player - the player that sent the packet. * @param packet - the packet that will be received by the server. */ public void receivePacket(Player player, Object packet) throws InvocationTargetException, IllegalAccessException { receivePacket(getChannel(player), packet); } /** * Pretend that a given packet has been received from a given client. * <p> * Note that {@link #onPacketInAsync(Player, Channel, Object)} will be invoked with this packet. * * @param channel - client identified by a channel. * @param packet - the packet that will be received by the server. */ public void receivePacket(Channel channel, Object packet) { channel.pipeline().context("encoder").fireChannelRead(packet); } /** * Retrieve the name of the channel injector, default implementation is "tiny-" + plugin name + "-" + a unique ID. * <p> * Note that this method will only be invoked once. It is no loger necessary to override this to support multiple instances. * * @return A unique channel handler name. */ protected String getHandlerName() { return plugin.getName() + "-" + ID.incrementAndGet(); } /** * Add a custom channel handler to the given player's channel pipeline, allowing us to intercept sent and received packets. * <p> * This will automatically be called when a player has logged in. * * @param player - the player to inject. */ public void injectPlayer(Player player) throws InvocationTargetException, IllegalAccessException { 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(handlerName); // Inject our packet interceptor if (interceptor == null) { interceptor = new PacketInterceptor(); channel.pipeline().addBefore("packet_handler", handlerName, interceptor); uninjectedChannels.remove(channel); } return interceptor; } catch (IllegalArgumentException e) { // Try again return (PacketInterceptor) channel.pipeline().get(handlerName); } } /** * Retrieve the Netty channel associated with a player. This is cached. * * @param player - the player. * @return The Netty channel. */ public Channel getChannel(Player player) throws IllegalAccessException, InvocationTargetException { Channel channel = channelLookup.get(player.getName()); // Lookup channel again if (channel == null) { ServerGamePacketListenerImpl connection = BukkitReflection.getServerPlayer(player).connection; channelLookup.put(player.getName(), channel = connection.connection.channel); } return channel; } /** * Uninject a specific player. * * @param player - the injected player. */ public void uninjectPlayer(Player player) throws InvocationTargetException, IllegalAccessException { uninjectChannel(getChannel(player)); } /** * Uninject a specific channel. * <p> * 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(() -> channel.pipeline().remove(handlerName)); } /** * 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) throws InvocationTargetException, IllegalAccessException { 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(handlerName) != null; } /** * Cease listening for packets. This is called automatically when your plugin is disabled. */ public final void close() throws InvocationTargetException, IllegalAccessException { if (!closed) { closed = true; // Remove our handlers for (Player player : plugin.getServer().getOnlinePlayers()) { uninjectPlayer(player); } // Clean up Bukkit HandlerList.unregisterAll(listener); unregisterChannelHandler(); } } /** * 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 = onPacketInAsync(player, channel, msg); } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Error in onPacketInAsync().", e); } if (msg != null) { super.channelRead(ctx, msg); } } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { try { msg = onPacketOutAsync(player, ctx.channel(), msg); } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Error in onPacketOutAsync().", e); } if (msg != null) { super.write(ctx, msg, promise); } } private void handleLoginStart(Channel channel, Object packet) throws IllegalAccessException { if (ClientboundGameProfilePacket.class.isInstance(packet)) { GameProfile profile = ((ClientboundGameProfilePacket)packet).getGameProfile(); channelLookup.put(profile.getName(), channel); } } } }