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.PlayerJoinEvent; 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. *
* 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. *
* 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 onPlayerJoin(PlayerJoinEvent 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. *
* 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. *
* 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. *
* 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. *
* 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. *
* 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. *
* 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. *
* 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. *
* 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. *
* 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); } } } }