package com.comphenix.tinyprotocol; import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import net.jitse.npclib.logging.NPCLibLogger; import net.minecraft.util.com.mojang.authlib.GameProfile; import net.minecraft.util.io.netty.channel.*; import org.bukkit.Bukkit; 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.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; /** * Minimized version of TinyProtocol by Kristian suited for NPCLib. */ public abstract class LegacyTinyProtocol { private static final AtomicInteger ID = new AtomicInteger(0); // Used in order to lookup a channel private static final Reflection.MethodInvoker getPlayerHandle = Reflection.getMethod("{obc}.entity.CraftPlayer", "getHandle"); private static final Reflection.FieldAccessor getConnection = Reflection.getField("{nms}.EntityPlayer", "playerConnection", Object.class); private static final Reflection.FieldAccessor getManager = Reflection.getField("{nms}.PlayerConnection", "networkManager", Object.class); private static final Reflection.FieldAccessor getChannel = Reflection.getField("{nms}.NetworkManager", Channel.class, 0); // Looking up ServerConnection private static final Class minecraftServerClass = Reflection.getUntypedClass("{nms}.MinecraftServer"); private static final Class serverConnectionClass = Reflection.getUntypedClass("{nms}.ServerConnection"); private static final Reflection.FieldAccessor getMinecraftServer = Reflection.getField("{obc}.CraftServer", minecraftServerClass, 0); private static final Reflection.FieldAccessor getServerConnection = Reflection.getField(minecraftServerClass, serverConnectionClass, 0); private static final Reflection.MethodInvoker getNetworkMarkers = Reflection.getTypedMethod(serverConnectionClass, null, List.class, serverConnectionClass); // Packets we have to intercept private static final Class PACKET_SET_PROTOCOL = Reflection.getMinecraftClass("PacketHandshakingInSetProtocol"); private static final Class PACKET_LOGIN_IN_START = Reflection.getMinecraftClass("PacketLoginInStart"); private static final Reflection.FieldAccessor getGameProfile = Reflection.getField(PACKET_LOGIN_IN_START, GameProfile.class, 0); private static final Reflection.FieldAccessor protocolId = Reflection.getField(PACKET_SET_PROTOCOL, int.class, 0); private static final Reflection.FieldAccessor protocolType = Reflection.getField(PACKET_SET_PROTOCOL, Enum.class, 0); // Speedup channel lookup private Map channelLookup = new MapMaker().weakValues().makeMap(); private Map protocolLookup = new MapMaker().weakKeys().makeMap(); private Listener listener; private Logger logger; // Channels that have already been removed private Set uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); // List of network markers private List networkManagers; // Injected channel handlers private List serverChannels = Lists.newArrayList(); private ChannelInboundHandlerAdapter serverChannelHandler; private ChannelInitializer beginInitProtocol; private ChannelInitializer endInitProtocol; // Current handler name private String handlerName; private volatile boolean closed; protected Plugin plugin; protected LegacyTinyProtocol(final Plugin plugin) { this.plugin = plugin; this.logger = new NPCLibLogger(plugin); // Compute handler name this.handlerName = "tiny-" + plugin.getName() + "-" + ID.incrementAndGet(); // Prepare existing players registerBukkitEvents(); try { logger.info("Attempting to inject into netty"); registerChannelHandler(); registerPlayers(plugin); } catch (IllegalArgumentException exceptionx) { // Damn you, late bind logger.log(Level.WARNING, "Attempting to delay injection"); new BukkitRunnable() { @Override public void run() { registerChannelHandler(); registerPlayers(plugin); logger.info("Injection complete"); } }.runTask(plugin); } } private void createServerChannelHandler() { // Handle connected channels endInitProtocol = new ChannelInitializer() { @SuppressWarnings("all") @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 exception) { logger.log(Level.SEVERE, "Cannot inject incomming channel " + channel, exception); } } }; // This is executed before Minecraft's channel handler beginInitProtocol = new ChannelInitializer() { @SuppressWarnings("all") @Override protected void initChannel(Channel channel) throws Exception { channel.pipeline().addLast(endInitProtocol); } }; serverChannelHandler = new ChannelInboundHandlerAdapter() { @SuppressWarnings("all") @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Channel channel = (Channel) msg; channel.pipeline().addFirst(beginInitProtocol); ctx.fireChannelRead(msg); } }; } private void registerBukkitEvents() { listener = new Listener() { @SuppressWarnings("unused") @EventHandler(priority = EventPriority.LOWEST) public final void onPlayerLogin(PlayerJoinEvent 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()); } } @SuppressWarnings("unused") @EventHandler public final void onPluginDisable(PluginDisableEvent e) { if (e.getPlugin().equals(plugin)) { close(); } } }; plugin.getServer().getPluginManager().registerEvents(listener, plugin); } @SuppressWarnings("unchecked") private void registerChannelHandler() { Object mcServer = getMinecraftServer.get(Bukkit.getServer()); Object serverConnection = getServerConnection.get(mcServer); boolean looking = true; // We need to synchronize against this list networkManagers = (List) getNetworkMarkers.invoke(null, serverConnection); createServerChannelHandler(); // Find the correct list, or implicitly throw an exception for (int i = 0; looking; i++) { List list = Reflection.getField(serverConnection.getClass(), List.class, i).get(serverConnection); for (Object item : list) { //if (!ChannelFuture.class.isInstance(item)) // break; // Channel future that contains the server connection Channel serverChannel = ((ChannelFuture) item).channel(); serverChannels.add(serverChannel); serverChannel.pipeline().addFirst(serverChannelHandler); logger.info("Server channel handler injected (" + serverChannel + ")"); looking = false; } } } private void unregisterChannelHandler() { if (serverChannelHandler == null) return; for (Channel serverChannel : serverChannels) { final ChannelPipeline pipeline = serverChannel.pipeline(); // Remove channel handler serverChannel.eventLoop().execute(() -> { try { pipeline.remove(serverChannelHandler); } catch (NoSuchElementException exception) { // That's fine } }); } } private void registerPlayers(Plugin plugin) { for (Player player : plugin.getServer().getOnlinePlayers()) { injectPlayer(player); } } public Object onPacketInAsync(Player sender, Object packet) { return packet; } private void injectPlayer(Player player) { injectChannelInternal(getChannel(player)).player = player; } 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 exception) { // Try again return (PacketInterceptor) channel.pipeline().get(handlerName); } } private Channel getChannel(Player player) { Channel channel = channelLookup.get(player.getName()); // Lookup channel again if (channel == null) { Object connection = getConnection.get(getPlayerHandle.invoke(player)); Object manager = getManager.get(connection); channelLookup.put(player.getName(), channel = getChannel.get(manager)); } return channel; } private 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)); } private void close() { if (!closed) { closed = true; // Remove our handlers for (Player player : plugin.getServer().getOnlinePlayers()) { uninjectChannel(getChannel(player)); } // Clean up Bukkit HandlerList.unregisterAll(listener); unregisterChannelHandler(); } } 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(); if (PACKET_LOGIN_IN_START.isInstance(msg)) { GameProfile profile = getGameProfile.get(msg); channelLookup.put(profile.getName(), channel); } else if (PACKET_SET_PROTOCOL.isInstance(msg)) { String protocol = protocolType.get(msg).name(); if (protocol.equalsIgnoreCase("LOGIN")) { protocolLookup.put(channel, protocolId.get(msg)); } } try { msg = onPacketInAsync(player, msg); } catch (Exception exception) { logger.log(Level.SEVERE, "Error in onPacketInAsync()", exception); } if (msg != null) { super.channelRead(ctx, msg); } } } }