diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java index dd7c5bbe..74bb63c2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java @@ -1,160 +1,167 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; - -import com.comphenix.protocol.async.AsyncListenerHandler; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.ListeningWhitelist; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.injector.BukkitUnwrapper; -import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; -import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; -import com.comphenix.protocol.injector.spigot.SpigotPacketInjector; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.reflect.MethodUtils; -import com.comphenix.protocol.reflect.ObjectWriter; -import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; -import com.comphenix.protocol.reflect.compiler.StructureCompiler; -import com.comphenix.protocol.reflect.instances.CollectionGenerator; -import com.comphenix.protocol.reflect.instances.DefaultInstances; -import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.comphenix.protocol.wrappers.ChunkPosition; -import com.comphenix.protocol.wrappers.WrappedDataWatcher; -import com.comphenix.protocol.wrappers.WrappedWatchableObject; -import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; - -/** - * Used to fix ClassLoader leaks that may lead to filling up the permanent generation. - * - * @author Kristian - */ -class CleanupStaticMembers { - - private ClassLoader loader; - private ErrorReporter reporter; - - public CleanupStaticMembers(ClassLoader loader, ErrorReporter reporter) { - this.loader = loader; - this.reporter = reporter; - } - - /** - * Ensure that the previous ClassLoader is not leaking. - */ - public void resetAll() { - // This list must always be updated - Class[] publicClasses = { - AsyncListenerHandler.class, ListeningWhitelist.class, PacketContainer.class, - BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class, - PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class, - BackgroundCompiler.class, StructureCompiler.class, - ObjectWriter.class, Packets.Server.class, Packets.Client.class, - ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class, - AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class, - MinecraftReflection.class, NbtBinarySerializer.class - }; - - String[] internalClasses = { - "com.comphenix.protocol.events.SerializedOfflinePlayer", - "com.comphenix.protocol.injector.player.InjectedServerConnection", - "com.comphenix.protocol.injector.player.NetworkFieldInjector", - "com.comphenix.protocol.injector.player.NetworkObjectInjector", - "com.comphenix.protocol.injector.player.NetworkServerInjector", - "com.comphenix.protocol.injector.player.PlayerInjector", - "com.comphenix.protocol.injector.EntityUtilities", - "com.comphenix.protocol.injector.packet.PacketRegistry", - "com.comphenix.protocol.injector.packet.PacketInjector", - "com.comphenix.protocol.injector.packet.ReadPacketModifier", - "com.comphenix.protocol.injector.StructureCache", - "com.comphenix.protocol.reflect.compiler.BoxingHelper", - "com.comphenix.protocol.reflect.compiler.MethodDescriptor", - "com.comphenix.protocol.wrappers.nbt.WrappedElement", - }; - - resetClasses(publicClasses); - resetClasses(getClasses(loader, internalClasses)); - } - - private void resetClasses(Class[] classes) { - // Reset each class one by one - for (Class clazz : classes) { - resetClass(clazz); - } - } - - private void resetClass(Class clazz) { - for (Field field : clazz.getFields()) { - Class type = field.getType(); - - // Only check static non-primitive fields. We also skip strings. - if (Modifier.isStatic(field.getModifiers()) && - !type.isPrimitive() && !type.equals(String.class)) { - - try { - setFinalStatic(field, null); - } catch (IllegalAccessException e) { - // Just inform the player - reporter.reportWarning(this, "Unable to reset field " + field.getName() + ": " + e.getMessage(), e); - } - } - } - } - - // HACK! HAACK! - private static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException { - int modifier = field.getModifiers(); - boolean isFinal = Modifier.isFinal(modifier); - - Field modifiersField = isFinal ? FieldUtils.getField(Field.class, "modifiers", true) : null; - - // We have to remove the final field first - if (isFinal) { - FieldUtils.writeField(modifiersField, field, modifier & ~Modifier.FINAL, true); - } - - // Now we can safely modify the field - FieldUtils.writeStaticField(field, newValue, true); - - // Revert modifier - if (isFinal) { - FieldUtils.writeField(modifiersField, field, modifier, true); - } - } - - private Class[] getClasses(ClassLoader loader, String[] names) { - List> output = new ArrayList>(); - - for (String name : names) { - try { - output.add(loader.loadClass(name)); - } catch (ClassNotFoundException e) { - // Warn the user - reporter.reportWarning(this, "Unable to unload class " + name, e); - } - } - - return output.toArray(new Class[0]); - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import com.comphenix.protocol.async.AsyncListenerHandler; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.injector.BukkitUnwrapper; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.injector.spigot.SpigotPacketInjector; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.MethodUtils; +import com.comphenix.protocol.reflect.ObjectWriter; +import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; +import com.comphenix.protocol.reflect.compiler.StructureCompiler; +import com.comphenix.protocol.reflect.instances.CollectionGenerator; +import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.ChunkPosition; +import com.comphenix.protocol.wrappers.WrappedDataWatcher; +import com.comphenix.protocol.wrappers.WrappedWatchableObject; +import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; + +/** + * Used to fix ClassLoader leaks that may lead to filling up the permanent generation. + * + * @author Kristian + */ +class CleanupStaticMembers { + // Reports + public final static ReportType REPORT_CANNOT_RESET_FIELD = new ReportType("Unable to reset field %s: %s"); + public final static ReportType REPORT_CANNOT_UNLOAD_CLASS = new ReportType("Unable to unload class %s."); + + private ClassLoader loader; + private ErrorReporter reporter; + + public CleanupStaticMembers(ClassLoader loader, ErrorReporter reporter) { + this.loader = loader; + this.reporter = reporter; + } + + /** + * Ensure that the previous ClassLoader is not leaking. + */ + public void resetAll() { + // This list must always be updated + Class[] publicClasses = { + AsyncListenerHandler.class, ListeningWhitelist.class, PacketContainer.class, + BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class, + PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class, + BackgroundCompiler.class, StructureCompiler.class, + ObjectWriter.class, Packets.Server.class, Packets.Client.class, + ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class, + AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class, + MinecraftReflection.class, NbtBinarySerializer.class + }; + + String[] internalClasses = { + "com.comphenix.protocol.events.SerializedOfflinePlayer", + "com.comphenix.protocol.injector.player.InjectedServerConnection", + "com.comphenix.protocol.injector.player.NetworkFieldInjector", + "com.comphenix.protocol.injector.player.NetworkObjectInjector", + "com.comphenix.protocol.injector.player.NetworkServerInjector", + "com.comphenix.protocol.injector.player.PlayerInjector", + "com.comphenix.protocol.injector.EntityUtilities", + "com.comphenix.protocol.injector.packet.PacketRegistry", + "com.comphenix.protocol.injector.packet.PacketInjector", + "com.comphenix.protocol.injector.packet.ReadPacketModifier", + "com.comphenix.protocol.injector.StructureCache", + "com.comphenix.protocol.reflect.compiler.BoxingHelper", + "com.comphenix.protocol.reflect.compiler.MethodDescriptor", + "com.comphenix.protocol.wrappers.nbt.WrappedElement", + }; + + resetClasses(publicClasses); + resetClasses(getClasses(loader, internalClasses)); + } + + private void resetClasses(Class[] classes) { + // Reset each class one by one + for (Class clazz : classes) { + resetClass(clazz); + } + } + + private void resetClass(Class clazz) { + for (Field field : clazz.getFields()) { + Class type = field.getType(); + + // Only check static non-primitive fields. We also skip strings. + if (Modifier.isStatic(field.getModifiers()) && + !type.isPrimitive() && !type.equals(String.class)) { + + try { + setFinalStatic(field, null); + } catch (IllegalAccessException e) { + // Just inform the player + reporter.reportWarning(this, + Report.newBuilder(REPORT_CANNOT_RESET_FIELD).error(e).messageParam(field.getName(), e.getMessage()) + ); + } + } + } + } + + // HACK! HAACK! + private static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException { + int modifier = field.getModifiers(); + boolean isFinal = Modifier.isFinal(modifier); + + Field modifiersField = isFinal ? FieldUtils.getField(Field.class, "modifiers", true) : null; + + // We have to remove the final field first + if (isFinal) { + FieldUtils.writeField(modifiersField, field, modifier & ~Modifier.FINAL, true); + } + + // Now we can safely modify the field + FieldUtils.writeStaticField(field, newValue, true); + + // Revert modifier + if (isFinal) { + FieldUtils.writeField(modifiersField, field, modifier, true); + } + } + + private Class[] getClasses(ClassLoader loader, String[] names) { + List> output = new ArrayList>(); + + for (String name : names) { + try { + output.add(loader.loadClass(name)); + } catch (ClassNotFoundException e) { + // Warn the user + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_UNLOAD_CLASS).error(e).messageParam(name)); + } + } + + return output.toArray(new Class[0]); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java index 1b8cd00c..7ca949bd 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java @@ -1,111 +1,117 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol; - -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; - -import com.comphenix.protocol.error.ErrorReporter; - -/** - * Base class for all our commands. - * - * @author Kristian - */ -abstract class CommandBase implements CommandExecutor { - - public static final String PERMISSION_ADMIN = "protocol.admin"; - - private String permission; - private String name; - private int minimumArgumentCount; - - protected ErrorReporter reporter; - - public CommandBase(ErrorReporter reporter, String permission, String name) { - this(reporter, permission, name, 0); - } - - public CommandBase(ErrorReporter reporter, String permission, String name, int minimumArgumentCount) { - this.reporter = reporter; - this.name = name; - this.permission = permission; - this.minimumArgumentCount = minimumArgumentCount; - } - - @Override - public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - try { - // Make sure we're dealing with the correct command - if (!command.getName().equalsIgnoreCase(name)) { - reporter.reportWarning(this, "Incorrect command assigned to " + this); - return false; - } - if (permission != null && !sender.hasPermission(permission)) { - sender.sendMessage(ChatColor.RED + "You haven't got permission to run this command."); - return true; - } - - // Check argument length - if (args != null && args.length >= minimumArgumentCount) { - return handleCommand(sender, args); - } else { - sender.sendMessage(ChatColor.RED + "Insufficient commands. You need at least " + minimumArgumentCount); - return false; - } - - } catch (Exception e) { - reporter.reportDetailed(this, "Cannot execute command " + name, e, sender, label, args); - return true; - } - } - - /** - * Retrieve the permission necessary to execute this command. - * @return The permission, or NULL if not needed. - */ - public String getPermission() { - return permission; - } - - /** - * Retrieve the primary name of this command. - * @return Primary name. - */ - public String getName() { - return name; - } - - /** - * Retrieve the error reporter. - * @return Error reporter. - */ - protected ErrorReporter getReporter() { - return reporter; - } - - /** - * Main implementation of this command. - * @param sender - command sender. - * @param args - * @return - */ - protected abstract boolean handleCommand(CommandSender sender, String[] args); -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol; + +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; + +/** + * Base class for all our commands. + * + * @author Kristian + */ +abstract class CommandBase implements CommandExecutor { + public static final ReportType REPORT_COMMAND_ERROR = new ReportType("Cannot execute command %s."); + public static final ReportType REPORT_UNEXPECTED_COMMAND = new ReportType("Incorrect command assigned to %s."); + + public static final String PERMISSION_ADMIN = "protocol.admin"; + + private String permission; + private String name; + private int minimumArgumentCount; + + protected ErrorReporter reporter; + + public CommandBase(ErrorReporter reporter, String permission, String name) { + this(reporter, permission, name, 0); + } + + public CommandBase(ErrorReporter reporter, String permission, String name, int minimumArgumentCount) { + this.reporter = reporter; + this.name = name; + this.permission = permission; + this.minimumArgumentCount = minimumArgumentCount; + } + + @Override + public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + try { + // Make sure we're dealing with the correct command + if (!command.getName().equalsIgnoreCase(name)) { + reporter.reportWarning(this, Report.newBuilder(REPORT_UNEXPECTED_COMMAND).messageParam(this)); + return false; + } + if (permission != null && !sender.hasPermission(permission)) { + sender.sendMessage(ChatColor.RED + "You haven't got permission to run this command."); + return true; + } + + // Check argument length + if (args != null && args.length >= minimumArgumentCount) { + return handleCommand(sender, args); + } else { + sender.sendMessage(ChatColor.RED + "Insufficient commands. You need at least " + minimumArgumentCount); + return false; + } + + } catch (Exception e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_COMMAND_ERROR).error(e).messageParam(name).callerParam(sender, label, args) + ); + return true; + } + } + + /** + * Retrieve the permission necessary to execute this command. + * @return The permission, or NULL if not needed. + */ + public String getPermission() { + return permission; + } + + /** + * Retrieve the primary name of this command. + * @return Primary name. + */ + public String getName() { + return name; + } + + /** + * Retrieve the error reporter. + * @return Error reporter. + */ + protected ErrorReporter getReporter() { + return reporter; + } + + /** + * Main implementation of this command. + * @param sender - command sender. + * @param args + * @return + */ + protected abstract boolean handleCommand(CommandSender sender, String[] args); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index 2a249286..392b1efc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -24,6 +24,8 @@ import org.bukkit.plugin.Plugin; import com.comphenix.protocol.MultipleLinesPrompt.MultipleConversationCanceller; import com.comphenix.protocol.concurrency.IntegerSet; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.events.PacketEvent; import com.google.common.collect.DiscreteDomains; import com.google.common.collect.Range; @@ -35,6 +37,12 @@ import com.google.common.collect.Ranges; * @author Kristian */ public class CommandFilter extends CommandBase { + public static final ReportType REPORT_FALLBACK_ENGINE = new ReportType("Falling back to the Rhino engine."); + public static final ReportType REPORT_CANNOT_LOAD_FALLBACK_ENGINE = new ReportType("Could not load Rhino either. Please upgrade your JVM or OS."); + public static final ReportType REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE = new ReportType("Unable to initialize packages for JavaScript engine."); + public static final ReportType REPORT_FILTER_REMOVED_FOR_ERROR = new ReportType("Removing filter %s for causing %s."); + public static final ReportType REPORT_CANNOT_HANDLE_CONVERSATION = new ReportType("Cannot handle conversation."); + public interface FilterFailedHandler{ /** * Invoked when a given filter has failed. @@ -236,7 +244,7 @@ public class CommandFilter extends CommandBase { printPackageWarning(e1); if (!config.getScriptEngineName().equals("rhino")) { - reporter.reportWarning(this, "Falling back to the Rhino engine."); + reporter.reportWarning(this, Report.newBuilder(REPORT_FALLBACK_ENGINE)); config.setScriptEngineName("rhino"); config.saveAll(); @@ -244,7 +252,7 @@ public class CommandFilter extends CommandBase { initializeEngine(); if (!isInitialized()) { - reporter.reportWarning(this, "Could not load Rhino either. Please upgrade your JVM or OS."); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_FALLBACK_ENGINE)); } } catch (ScriptException e2) { // And again .. @@ -255,7 +263,7 @@ public class CommandFilter extends CommandBase { } private void printPackageWarning(ScriptException e) { - reporter.reportWarning(this, "Unable to initialize packages for JavaScript engine.", e); + reporter.reportWarning(this, Report.newBuilder(REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE).error(e)); } /** @@ -288,7 +296,9 @@ public class CommandFilter extends CommandBase { @Override public boolean handle(PacketEvent event, Filter filter, Exception ex) { reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event); - reporter.reportWarning(this, "Removing filter " + filter.getName() + " for causing an exception."); + reporter.reportWarning(this, + Report.newBuilder(REPORT_FILTER_REMOVED_FOR_ERROR).messageParam(filter.getName(), ex.getClass().getSimpleName()) + ); return false; } }; @@ -398,7 +408,9 @@ public class CommandFilter extends CommandBase { whom.sendRawMessage(ChatColor.RED + "Cancelled filter."); } } catch (Exception e) { - reporter.reportDetailed(this, "Cannot handle conversation.", e, event); + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_HANDLE_CONVERSATION).error(e).callerParam(event) + ); } } }). diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index d130677d..a12b5c50 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -1,538 +1,545 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol; - -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.sf.cglib.proxy.Factory; - -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -import com.comphenix.protocol.concurrency.AbstractIntervalTree; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.ConnectionSide; -import com.comphenix.protocol.events.ListenerPriority; -import com.comphenix.protocol.events.ListeningWhitelist; -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.events.PacketListener; -import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.PrettyPrinter; -import com.comphenix.protocol.utility.ChatExtensions; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.google.common.collect.DiscreteDomains; -import com.google.common.collect.Range; -import com.google.common.collect.Ranges; -import com.google.common.collect.Sets; - -/** - * Handles the "packet" debug command. - * - * @author Kristian - */ -class CommandPacket extends CommandBase { - - private interface DetailedPacketListener extends PacketListener { - /** - * Determine whether or not the given packet listener is detailed or not. - * @return TRUE if it is detailed, FALSE otherwise. - */ - public boolean isDetailed(); - } - - private enum SubCommand { - ADD, REMOVE, NAMES, PAGE; - } - - /** - * Name of this command. - */ - public static final String NAME = "packet"; - - /** - * Number of lines per page. - */ - public static final int PAGE_LINE_COUNT = 9; - - private Plugin plugin; - private Logger logger; - private ProtocolManager manager; - - private ChatExtensions chatter; - - // Paged message - private Map> pagedMessage = new WeakHashMap>(); - - // Registered packet listeners - private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); - private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); - - // Filter packet events - private CommandFilter filter; - - public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) { - super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); - this.plugin = plugin; - this.logger = logger; - this.manager = manager; - this.filter = filter; - this.chatter = new ChatExtensions(manager); - } - - /** - * Construct a packet listener interval tree. - * @return Construct the tree. - */ - private AbstractIntervalTree createTree(final ConnectionSide side) { - return new AbstractIntervalTree() { - @Override - protected Integer decrementKey(Integer key) { - return key != null ? key - 1 : null; - } - - @Override - protected Integer incrementKey(Integer key) { - return key != null ? key + 1 : null; - } - - @Override - protected void onEntryAdded(Entry added) { - // Ensure that the starting ID and the ending ID is correct - // This is necessary because the interval tree may change the range. - if (added != null) { - Range key = added.getKey(); - DetailedPacketListener listener = added.getValue(); - DetailedPacketListener corrected = createPacketListener( - side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed()); - - added.setValue(corrected); - - if (corrected != null) { - manager.addPacketListener(corrected); - } else { - // Never mind - remove(key.lowerEndpoint(), key.upperEndpoint()); - } - } - } - - @Override - protected void onEntryRemoved(Entry removed) { - // Remove the listener - if (removed != null) { - DetailedPacketListener listener = removed.getValue(); - - if (listener != null) { - manager.removePacketListener(listener); - } - } - } - }; - } - - /** - * Send a message without invoking the packet listeners. - * @param receiver - the player to send it to. - * @param message - the message to send. - * @return TRUE if the message was sent successfully, FALSE otherwise. - */ - public void sendMessageSilently(CommandSender receiver, String message) { - try { - chatter.sendMessageSilently(receiver, message); - } catch (InvocationTargetException e) { - reporter.reportDetailed(this, "Cannot send chat message.", e, receiver, message); - } - } - - /** - * Broadcast a message without invoking any packet listeners. - * @param message - message to send. - * @param permission - permission required to receieve the message. NULL to target everyone. - */ - public void broadcastMessageSilently(String message, String permission) { - try { - chatter.broadcastMessageSilently(message, permission); - } catch (InvocationTargetException e) { - reporter.reportDetailed(this, "Cannot send chat message.", e, message, permission); - } - } - - private void printPage(CommandSender sender, int pageIndex) { - List paged = pagedMessage.get(sender); - - // Make sure the player has any pages - if (paged != null) { - int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1; - - for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) { - if (i < paged.size()) { - sendMessageSilently(sender, " " + paged.get(i)); - } - } - - // More data? - if (pageIndex < lastPage) { - sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page."); - } - - } else { - sendMessageSilently(sender, ChatColor.RED + "No pages found."); - } - } - - /* - * Description: Adds or removes a simple packet listener. - Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] - */ - @Override - protected boolean handleCommand(CommandSender sender, String[] args) { - try { - SubCommand subCommand = parseCommand(args, 0); - - // Commands with different parameters - if (subCommand == SubCommand.PAGE) { - int page = Integer.parseInt(args[1]); - - if (page > 0) - printPage(sender, page); - else - sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero."); - return true; - } - - ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH); - - Integer lastIndex = args.length - 1; - Boolean detailed = parseBoolean(args, "detailed", lastIndex); - - // See if the last element is a boolean - if (detailed == null) { - detailed = false; - } else { - lastIndex--; - } - - // Make sure the packet IDs are valid - List> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255)); - - if (ranges.isEmpty()) { - // Use every packet ID - ranges.add(Ranges.closed(0, 255)); - } - - // Perform commands - if (subCommand == SubCommand.ADD) { - // The add command is dangerous - don't default on the connection side - if (args.length == 1) { - sender.sendMessage(ChatColor.RED + "Please specify a connectionn side."); - return false; - } - - executeAddCommand(sender, side, detailed, ranges); - } else if (subCommand == SubCommand.REMOVE) { - executeRemoveCommand(sender, side, detailed, ranges); - } else if (subCommand == SubCommand.NAMES) { - executeNamesCommand(sender, side, ranges); - } - - } catch (NumberFormatException e) { - sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage()); - } catch (IllegalArgumentException e) { - sendMessageSilently(sender, ChatColor.RED + e.getMessage()); - } - - return true; - } - - private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { - for (Range range : ranges) { - DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed); - sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener)); - } - } - - private void executeRemoveCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { - int count = 0; - - // Remove each packet listener - for (Range range : ranges) { - count += removePacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed).size(); - } - - sendMessageSilently(sender, ChatColor.BLUE + "Fully removed " + count + " listeners."); - } - - private void executeNamesCommand(CommandSender sender, ConnectionSide side, List> ranges) { - Set named = getNamedPackets(side); - List messages = new ArrayList(); - - // Print the equivalent name of every given ID - for (Range range : ranges) { - for (int id : range.asSet(DiscreteDomains.integers())) { - if (named.contains(id)) { - messages.add(ChatColor.WHITE + "" + id + ": " + ChatColor.BLUE + Packets.getDeclaredName(id)); - } - } - } - - if (sender instanceof Player && messages.size() > 0 && messages.size() > PAGE_LINE_COUNT) { - // Divide the messages into chuncks - pagedMessage.put(sender, messages); - printPage(sender, 1); - - } else { - // Just print the damn thing - for (String message : messages) { - sendMessageSilently(sender, message); - } - } - } - - /** - * Retrieve whitelist information about a given listener. - * @param listener - the given listener. - * @return Whitelist information. - */ - private String getWhitelistInfo(PacketListener listener) { - boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist()); - boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist()); - - if (!sendingEmpty && !receivingEmpty) - return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist()); - else if (!sendingEmpty) - return listener.getSendingWhitelist().toString(); - else if (!receivingEmpty) - return listener.getReceivingWhitelist().toString(); - else - return "[None]"; - } - - private Set getValidPackets(ConnectionSide side) throws FieldAccessException { - HashSet supported = Sets.newHashSet(); - - if (side.isForClient()) - supported.addAll(Packets.Client.getSupported()); - else if (side.isForServer()) - supported.addAll(Packets.Server.getSupported()); - - System.out.println("Supported for " + side + ": " + supported); - return supported; - } - - private Set getNamedPackets(ConnectionSide side) { - - Set valids = null; - Set result = Sets.newHashSet(); - - try { - valids = getValidPackets(side); - } catch (FieldAccessException e) { - valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers()); - } - - // Check connection side - if (side.isForClient()) - result.addAll(Packets.Client.getRegistry().values()); - if (side.isForServer()) - result.addAll(Packets.Server.getRegistry().values()); - - // Remove invalid packets - result.retainAll(valids); - return result; - } - - public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) { - Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers()); - Set packets; - - try { - // Only use supported packet IDs - packets = new HashSet(getValidPackets(side)); - packets.retainAll(range); - - } catch (FieldAccessException e) { - // Don't filter anything then - packets = range; - } - - // Ignore empty sets - if (packets.isEmpty()) - return null; - - // Create the listener we will be using - final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH); - - return new DetailedPacketListener() { - @Override - public void onPacketSending(PacketEvent event) { - if (side.isForServer() && filter.filterEvent(event)) { - printInformation(event); - } - } - - @Override - public void onPacketReceiving(PacketEvent event) { - if (side.isForClient() && filter.filterEvent(event)) { - printInformation(event); - } - } - - private void printInformation(PacketEvent event) { - String format = side.isForClient() ? - "Received %s (%s) from %s" : - "Sent %s (%s) to %s"; - String shortDescription = String.format(format, - Packets.getDeclaredName(event.getPacketID()), - event.getPacketID(), - event.getPlayer().getName() - ); - - // Detailed will print the packet's content too - if (detailed) { - try { - Object packet = event.getPacket().getHandle(); - Class clazz = packet.getClass(); - - // Get the first Minecraft super class - while ((!MinecraftReflection.isMinecraftClass(clazz) || - Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) { - clazz = clazz.getSuperclass(); - } - - logger.info(shortDescription + ":\n" + - PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass()) - ); - - } catch (IllegalAccessException e) { - logger.log(Level.WARNING, "Unable to use reflection.", e); - } - } else { - logger.info(shortDescription + "."); - } - } - - @Override - public ListeningWhitelist getSendingWhitelist() { - return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; - } - - @Override - public ListeningWhitelist getReceivingWhitelist() { - return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; - } - - @Override - public Plugin getPlugin() { - return plugin; - } - - @Override - public boolean isDetailed() { - return detailed; - } - }; - } - - public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { - DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed); - - // The trees will manage the listeners for us - if (listener != null) { - if (side.isForClient()) - clientListeners.put(idStart, idStop, listener); - if (side.isForServer()) - serverListeners.put(idStart, idStop, listener); - return listener; - } else { - throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + "."); - } - } - - public Set.Entry> removePacketListeners( - ConnectionSide side, int idStart, int idStop, boolean detailed) { - - HashSet.Entry> result = Sets.newHashSet(); - - // The interval tree will automatically remove the listeners for us - if (side.isForClient()) - result.addAll(clientListeners.remove(idStart, idStop, true)); - if (side.isForServer()) - result.addAll(serverListeners.remove(idStart, idStop, true)); - return result; - } - - private SubCommand parseCommand(String[] args, int index) { - String text = args[index].toLowerCase(); - - // Parse this too - if ("add".startsWith(text)) - return SubCommand.ADD; - else if ("remove".startsWith(text)) - return SubCommand.REMOVE; - else if ("names".startsWith(text)) - return SubCommand.NAMES; - else if ("page".startsWith(text)) - return SubCommand.PAGE; - else - throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove."); - } - - private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) { - if (index < args.length) { - String text = args[index].toLowerCase(); - - // Parse the side gracefully - if ("client".startsWith(text)) - return ConnectionSide.CLIENT_SIDE; - else if ("server".startsWith(text)) - return ConnectionSide.SERVER_SIDE; - else - throw new IllegalArgumentException(text + " is not a connection side."); - - } else { - return defaultValue; - } - } - - // Parse a boolean - private Boolean parseBoolean(String[] args, String parameterName, int index) { - if (index < args.length) { - if (args[index].equalsIgnoreCase("true")) - return true; - else if (args[index].equalsIgnoreCase(parameterName)) - return true; - else if (args[index].equalsIgnoreCase("false")) - return false; - else - return null; - } else { - return null; - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.sf.cglib.proxy.Factory; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.concurrency.AbstractIntervalTree; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.PrettyPrinter; +import com.comphenix.protocol.utility.ChatExtensions; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.DiscreteDomains; +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; +import com.google.common.collect.Sets; + +/** + * Handles the "packet" debug command. + * + * @author Kristian + */ +class CommandPacket extends CommandBase { + public static final ReportType REPORT_CANNOT_SEND_MESSAGE = new ReportType("Cannot send chat message."); + + private interface DetailedPacketListener extends PacketListener { + /** + * Determine whether or not the given packet listener is detailed or not. + * @return TRUE if it is detailed, FALSE otherwise. + */ + public boolean isDetailed(); + } + + private enum SubCommand { + ADD, REMOVE, NAMES, PAGE; + } + + /** + * Name of this command. + */ + public static final String NAME = "packet"; + + /** + * Number of lines per page. + */ + public static final int PAGE_LINE_COUNT = 9; + + private Plugin plugin; + private Logger logger; + private ProtocolManager manager; + + private ChatExtensions chatter; + + // Paged message + private Map> pagedMessage = new WeakHashMap>(); + + // Registered packet listeners + private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); + private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); + + // Filter packet events + private CommandFilter filter; + + public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) { + super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); + this.plugin = plugin; + this.logger = logger; + this.manager = manager; + this.filter = filter; + this.chatter = new ChatExtensions(manager); + } + + /** + * Construct a packet listener interval tree. + * @return Construct the tree. + */ + private AbstractIntervalTree createTree(final ConnectionSide side) { + return new AbstractIntervalTree() { + @Override + protected Integer decrementKey(Integer key) { + return key != null ? key - 1 : null; + } + + @Override + protected Integer incrementKey(Integer key) { + return key != null ? key + 1 : null; + } + + @Override + protected void onEntryAdded(Entry added) { + // Ensure that the starting ID and the ending ID is correct + // This is necessary because the interval tree may change the range. + if (added != null) { + Range key = added.getKey(); + DetailedPacketListener listener = added.getValue(); + DetailedPacketListener corrected = createPacketListener( + side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed()); + + added.setValue(corrected); + + if (corrected != null) { + manager.addPacketListener(corrected); + } else { + // Never mind + remove(key.lowerEndpoint(), key.upperEndpoint()); + } + } + } + + @Override + protected void onEntryRemoved(Entry removed) { + // Remove the listener + if (removed != null) { + DetailedPacketListener listener = removed.getValue(); + + if (listener != null) { + manager.removePacketListener(listener); + } + } + } + }; + } + + /** + * Send a message without invoking the packet listeners. + * @param receiver - the player to send it to. + * @param message - the message to send. + * @return TRUE if the message was sent successfully, FALSE otherwise. + */ + public void sendMessageSilently(CommandSender receiver, String message) { + try { + chatter.sendMessageSilently(receiver, message); + } catch (InvocationTargetException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(receiver, message) + ); + } + } + + /** + * Broadcast a message without invoking any packet listeners. + * @param message - message to send. + * @param permission - permission required to receieve the message. NULL to target everyone. + */ + public void broadcastMessageSilently(String message, String permission) { + try { + chatter.broadcastMessageSilently(message, permission); + } catch (InvocationTargetException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(message, permission) + ); + } + } + + private void printPage(CommandSender sender, int pageIndex) { + List paged = pagedMessage.get(sender); + + // Make sure the player has any pages + if (paged != null) { + int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1; + + for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) { + if (i < paged.size()) { + sendMessageSilently(sender, " " + paged.get(i)); + } + } + + // More data? + if (pageIndex < lastPage) { + sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page."); + } + + } else { + sendMessageSilently(sender, ChatColor.RED + "No pages found."); + } + } + + /* + * Description: Adds or removes a simple packet listener. + Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] + */ + @Override + protected boolean handleCommand(CommandSender sender, String[] args) { + try { + SubCommand subCommand = parseCommand(args, 0); + + // Commands with different parameters + if (subCommand == SubCommand.PAGE) { + int page = Integer.parseInt(args[1]); + + if (page > 0) + printPage(sender, page); + else + sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero."); + return true; + } + + ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH); + + Integer lastIndex = args.length - 1; + Boolean detailed = parseBoolean(args, "detailed", lastIndex); + + // See if the last element is a boolean + if (detailed == null) { + detailed = false; + } else { + lastIndex--; + } + + // Make sure the packet IDs are valid + List> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255)); + + if (ranges.isEmpty()) { + // Use every packet ID + ranges.add(Ranges.closed(0, 255)); + } + + // Perform commands + if (subCommand == SubCommand.ADD) { + // The add command is dangerous - don't default on the connection side + if (args.length == 1) { + sender.sendMessage(ChatColor.RED + "Please specify a connectionn side."); + return false; + } + + executeAddCommand(sender, side, detailed, ranges); + } else if (subCommand == SubCommand.REMOVE) { + executeRemoveCommand(sender, side, detailed, ranges); + } else if (subCommand == SubCommand.NAMES) { + executeNamesCommand(sender, side, ranges); + } + + } catch (NumberFormatException e) { + sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage()); + } catch (IllegalArgumentException e) { + sendMessageSilently(sender, ChatColor.RED + e.getMessage()); + } + + return true; + } + + private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { + for (Range range : ranges) { + DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed); + sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener)); + } + } + + private void executeRemoveCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { + int count = 0; + + // Remove each packet listener + for (Range range : ranges) { + count += removePacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed).size(); + } + + sendMessageSilently(sender, ChatColor.BLUE + "Fully removed " + count + " listeners."); + } + + private void executeNamesCommand(CommandSender sender, ConnectionSide side, List> ranges) { + Set named = getNamedPackets(side); + List messages = new ArrayList(); + + // Print the equivalent name of every given ID + for (Range range : ranges) { + for (int id : range.asSet(DiscreteDomains.integers())) { + if (named.contains(id)) { + messages.add(ChatColor.WHITE + "" + id + ": " + ChatColor.BLUE + Packets.getDeclaredName(id)); + } + } + } + + if (sender instanceof Player && messages.size() > 0 && messages.size() > PAGE_LINE_COUNT) { + // Divide the messages into chuncks + pagedMessage.put(sender, messages); + printPage(sender, 1); + + } else { + // Just print the damn thing + for (String message : messages) { + sendMessageSilently(sender, message); + } + } + } + + /** + * Retrieve whitelist information about a given listener. + * @param listener - the given listener. + * @return Whitelist information. + */ + private String getWhitelistInfo(PacketListener listener) { + boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist()); + boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist()); + + if (!sendingEmpty && !receivingEmpty) + return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist()); + else if (!sendingEmpty) + return listener.getSendingWhitelist().toString(); + else if (!receivingEmpty) + return listener.getReceivingWhitelist().toString(); + else + return "[None]"; + } + + private Set getValidPackets(ConnectionSide side) throws FieldAccessException { + HashSet supported = Sets.newHashSet(); + + if (side.isForClient()) + supported.addAll(Packets.Client.getSupported()); + else if (side.isForServer()) + supported.addAll(Packets.Server.getSupported()); + + System.out.println("Supported for " + side + ": " + supported); + return supported; + } + + private Set getNamedPackets(ConnectionSide side) { + + Set valids = null; + Set result = Sets.newHashSet(); + + try { + valids = getValidPackets(side); + } catch (FieldAccessException e) { + valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers()); + } + + // Check connection side + if (side.isForClient()) + result.addAll(Packets.Client.getRegistry().values()); + if (side.isForServer()) + result.addAll(Packets.Server.getRegistry().values()); + + // Remove invalid packets + result.retainAll(valids); + return result; + } + + public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) { + Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers()); + Set packets; + + try { + // Only use supported packet IDs + packets = new HashSet(getValidPackets(side)); + packets.retainAll(range); + + } catch (FieldAccessException e) { + // Don't filter anything then + packets = range; + } + + // Ignore empty sets + if (packets.isEmpty()) + return null; + + // Create the listener we will be using + final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH); + + return new DetailedPacketListener() { + @Override + public void onPacketSending(PacketEvent event) { + if (side.isForServer() && filter.filterEvent(event)) { + printInformation(event); + } + } + + @Override + public void onPacketReceiving(PacketEvent event) { + if (side.isForClient() && filter.filterEvent(event)) { + printInformation(event); + } + } + + private void printInformation(PacketEvent event) { + String format = side.isForClient() ? + "Received %s (%s) from %s" : + "Sent %s (%s) to %s"; + String shortDescription = String.format(format, + Packets.getDeclaredName(event.getPacketID()), + event.getPacketID(), + event.getPlayer().getName() + ); + + // Detailed will print the packet's content too + if (detailed) { + try { + Object packet = event.getPacket().getHandle(); + Class clazz = packet.getClass(); + + // Get the first Minecraft super class + while ((!MinecraftReflection.isMinecraftClass(clazz) || + Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) { + clazz = clazz.getSuperclass(); + } + + logger.info(shortDescription + ":\n" + + PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass()) + ); + + } catch (IllegalAccessException e) { + logger.log(Level.WARNING, "Unable to use reflection.", e); + } + } else { + logger.info(shortDescription + "."); + } + } + + @Override + public ListeningWhitelist getSendingWhitelist() { + return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; + } + + @Override + public ListeningWhitelist getReceivingWhitelist() { + return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; + } + + @Override + public Plugin getPlugin() { + return plugin; + } + + @Override + public boolean isDetailed() { + return detailed; + } + }; + } + + public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { + DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed); + + // The trees will manage the listeners for us + if (listener != null) { + if (side.isForClient()) + clientListeners.put(idStart, idStop, listener); + if (side.isForServer()) + serverListeners.put(idStart, idStop, listener); + return listener; + } else { + throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + "."); + } + } + + public Set.Entry> removePacketListeners( + ConnectionSide side, int idStart, int idStop, boolean detailed) { + + HashSet.Entry> result = Sets.newHashSet(); + + // The interval tree will automatically remove the listeners for us + if (side.isForClient()) + result.addAll(clientListeners.remove(idStart, idStop, true)); + if (side.isForServer()) + result.addAll(serverListeners.remove(idStart, idStop, true)); + return result; + } + + private SubCommand parseCommand(String[] args, int index) { + String text = args[index].toLowerCase(); + + // Parse this too + if ("add".startsWith(text)) + return SubCommand.ADD; + else if ("remove".startsWith(text)) + return SubCommand.REMOVE; + else if ("names".startsWith(text)) + return SubCommand.NAMES; + else if ("page".startsWith(text)) + return SubCommand.PAGE; + else + throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove."); + } + + private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) { + if (index < args.length) { + String text = args[index].toLowerCase(); + + // Parse the side gracefully + if ("client".startsWith(text)) + return ConnectionSide.CLIENT_SIDE; + else if ("server".startsWith(text)) + return ConnectionSide.SERVER_SIDE; + else + throw new IllegalArgumentException(text + " is not a connection side."); + + } else { + return defaultValue; + } + } + + // Parse a boolean + private Boolean parseBoolean(String[] args, String parameterName, int index) { + if (index < args.length) { + if (args[index].equalsIgnoreCase("true")) + return true; + else if (args[index].equalsIgnoreCase(parameterName)) + return true; + else if (args[index].equalsIgnoreCase("false")) + return false; + else + return null; + } else { + return null; + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java index 672c4127..4509a504 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java @@ -1,137 +1,147 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol; - -import java.io.IOException; - -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.plugin.Plugin; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.metrics.Updater; -import com.comphenix.protocol.metrics.Updater.UpdateResult; -import com.comphenix.protocol.metrics.Updater.UpdateType; -import com.comphenix.protocol.utility.WrappedScheduler; - -/** - * Handles the "protocol" administration command. - * - * @author Kristian - */ -class CommandProtocol extends CommandBase { - /** - * Name of this command. - */ - public static final String NAME = "protocol"; - - private Plugin plugin; - private Updater updater; - private ProtocolConfig config; - - public CommandProtocol(ErrorReporter reporter, Plugin plugin, Updater updater, ProtocolConfig config) { - super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); - this.plugin = plugin; - this.updater = updater; - this.config = config; - } - - @Override - protected boolean handleCommand(CommandSender sender, String[] args) { - String subCommand = args[0]; - - // Only return TRUE if we executed the correct command - if (subCommand.equalsIgnoreCase("config") || subCommand.equalsIgnoreCase("reload")) - reloadConfiguration(sender); - else if (subCommand.equalsIgnoreCase("check")) - checkVersion(sender); - else if (subCommand.equalsIgnoreCase("update")) - updateVersion(sender); - else - return false; - return true; - } - - public void checkVersion(final CommandSender sender) { - // Perform on an async thread - WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() { - @Override - public void run() { - try { - UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true); - sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString()); - } catch (Exception e) { - if (isHttpError(e)) { - getReporter().reportWarning(this, "Http error: " + e.getCause().getMessage()); - } else { - getReporter().reportDetailed(this, "Cannot check updates for ProtocolLib.", e, sender); - } - } - } - }, 0L); - - updateFinished(); - } - - public void updateVersion(final CommandSender sender) { - // Perform on an async thread - WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() { - @Override - public void run() { - try { - UpdateResult result = updater.update(UpdateType.DEFAULT, true); - sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString()); - } catch (Exception e) { - if (isHttpError(e)) { - getReporter().reportWarning(this, "Http error: " + e.getCause().getMessage()); - } else { - getReporter().reportDetailed(this, "Cannot update ProtocolLib.", e, sender); - } - } - } - }, 0L); - - updateFinished(); - } - - private boolean isHttpError(Exception e) { - Throwable cause = e.getCause(); - - if (cause instanceof IOException) { - // Thanks for making the message a part of the API ... - return cause.getMessage().contains("HTTP response"); - } else { - return false; - } - } - - /** - * Prevent further automatic updates until the next delay. - */ - public void updateFinished() { - long currentTime = System.currentTimeMillis() / ProtocolLibrary.MILLI_PER_SECOND; - - config.setAutoLastTime(currentTime); - config.saveAll(); - } - - public void reloadConfiguration(CommandSender sender) { - plugin.reloadConfig(); - sender.sendMessage(ChatColor.BLUE + "Reloaded configuration!"); - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol; + +import java.io.IOException; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.metrics.Updater; +import com.comphenix.protocol.metrics.Updater.UpdateResult; +import com.comphenix.protocol.metrics.Updater.UpdateType; +import com.comphenix.protocol.utility.WrappedScheduler; + +/** + * Handles the "protocol" administration command. + * + * @author Kristian + */ +class CommandProtocol extends CommandBase { + /** + * Name of this command. + */ + public static final String NAME = "protocol"; + + public static final ReportType REPORT_HTTP_ERROR = new ReportType("Http error: %s"); + public static final ReportType REPORT_CANNOT_CHECK_FOR_UPDATES = new ReportType("Cannot check updates for ProtocolLib."); + public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot update ProtocolLib."); + + private Plugin plugin; + private Updater updater; + private ProtocolConfig config; + + public CommandProtocol(ErrorReporter reporter, Plugin plugin, Updater updater, ProtocolConfig config) { + super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); + this.plugin = plugin; + this.updater = updater; + this.config = config; + } + + @Override + protected boolean handleCommand(CommandSender sender, String[] args) { + String subCommand = args[0]; + + // Only return TRUE if we executed the correct command + if (subCommand.equalsIgnoreCase("config") || subCommand.equalsIgnoreCase("reload")) + reloadConfiguration(sender); + else if (subCommand.equalsIgnoreCase("check")) + checkVersion(sender); + else if (subCommand.equalsIgnoreCase("update")) + updateVersion(sender); + else + return false; + return true; + } + + public void checkVersion(final CommandSender sender) { + // Perform on an async thread + WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() { + @Override + public void run() { + try { + UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true); + sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString()); + } catch (Exception e) { + if (isHttpError(e)) { + getReporter().reportWarning(this, + Report.newBuilder(REPORT_HTTP_ERROR).messageParam(e.getCause().getMessage()) + ); + } else { + getReporter().reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CHECK_FOR_UPDATES).error(e).callerParam(sender)); + } + } + } + }, 0L); + + updateFinished(); + } + + public void updateVersion(final CommandSender sender) { + // Perform on an async thread + WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() { + @Override + public void run() { + try { + UpdateResult result = updater.update(UpdateType.DEFAULT, true); + sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString()); + } catch (Exception e) { + if (isHttpError(e)) { + getReporter().reportWarning(this, + Report.newBuilder(REPORT_HTTP_ERROR).messageParam(e.getCause().getMessage()) + ); + } else { + getReporter().reportDetailed(this,Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e).callerParam(sender)); + } + } + } + }, 0L); + + updateFinished(); + } + + private boolean isHttpError(Exception e) { + Throwable cause = e.getCause(); + + if (cause instanceof IOException) { + // Thanks for making the message a part of the API ... + return cause.getMessage().contains("HTTP response"); + } else { + return false; + } + } + + /** + * Prevent further automatic updates until the next delay. + */ + public void updateFinished() { + long currentTime = System.currentTimeMillis() / ProtocolLibrary.MILLI_PER_SECOND; + + config.setAutoLastTime(currentTime); + config.saveAll(); + } + + public void reloadConfiguration(CommandSender sender) { + plugin.reloadConfig(); + sender.sendMessage(ChatColor.BLUE + "Reloaded configuration!"); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 3071707f..8fead494 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -35,6 +35,8 @@ import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.error.DetailedErrorReporter; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.injector.DelayedSingleTask; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; @@ -51,6 +53,24 @@ import com.comphenix.protocol.utility.MinecraftVersion; * @author Kristian */ public class ProtocolLibrary extends JavaPlugin { + // Every possible error or warning report type + public static final ReportType REPORT_CANNOT_LOAD_CONFIG = new ReportType("Cannot load configuration"); + public static final ReportType REPORT_CANNOT_DELETE_CONFIG = new ReportType("Cannot delete old ProtocolLib configuration."); + public static final ReportType REPORT_CANNOT_PARSE_INJECTION_METHOD = new ReportType("Cannot parse injection method. Using default."); + + public static final ReportType REPORT_PLUGIN_LOAD_ERROR = new ReportType("Cannot load ProtocolLib."); + public static final ReportType REPORT_PLUGIN_ENABLE_ERROR = new ReportType("Cannot enable ProtocolLib."); + + public static final ReportType REPORT_METRICS_IO_ERROR = new ReportType("Unable to enable metrics due to network problems."); + public static final ReportType REPORT_METRICS_GENERIC_ERROR = new ReportType("Unable to enable metrics due to network problems."); + + public static final ReportType REPORT_CANNOT_PARSE_MINECRAFT_VERSION = new ReportType("Unable to retrieve current Minecraft version."); + public static final ReportType REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS = new ReportType("Unable to detect conflicting plugin versions."); + public static final ReportType REPORT_CANNOT_REGISTER_COMMAND = new ReportType("Cannot register command %s: %s"); + + public static final ReportType REPORT_CANNOT_CREATE_TIMEOUT_TASK = new ReportType("Unable to create packet timeout task."); + public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot perform automatic updates."); + /** * The minimum version ProtocolLib has been tested with. */ @@ -120,13 +140,13 @@ public class ProtocolLibrary extends JavaPlugin { try { config = new ProtocolConfig(this); } catch (Exception e) { - detailedReporter.reportWarning(this, "Cannot load configuration", e); + detailedReporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_CONFIG).error(e)); // Load it again if (deleteConfig()) { config = new ProtocolConfig(this); } else { - reporter.reportWarning(this, "Cannot delete old ProtocolLib configuration."); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_DELETE_CONFIG)); } } @@ -162,7 +182,7 @@ public class ProtocolLibrary extends JavaPlugin { protocolManager.setPlayerHook(hook); } } catch (IllegalArgumentException e) { - detailedReporter.reportWarning(config, "Cannot parse injection method. Using default.", e); + detailedReporter.reportWarning(config, Report.newBuilder(REPORT_CANNOT_PARSE_INJECTION_METHOD).error(e)); } // Initialize command handlers @@ -174,7 +194,7 @@ public class ProtocolLibrary extends JavaPlugin { setupBroadcastUsers(PERMISSION_INFO); } catch (Throwable e) { - detailedReporter.reportDetailed(this, "Cannot load ProtocolLib.", e, protocolManager); + detailedReporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_LOAD_ERROR).error(e).callerParam(protocolManager)); disablePlugin(); } } @@ -273,7 +293,7 @@ public class ProtocolLibrary extends JavaPlugin { createAsyncTask(server); } catch (Throwable e) { - reporter.reportDetailed(this, "Cannot enable ProtocolLib.", e); + reporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_ENABLE_ERROR).error(e)); disablePlugin(); return; } @@ -284,9 +304,9 @@ public class ProtocolLibrary extends JavaPlugin { statistisc = new Statistics(this); } } catch (IOException e) { - reporter.reportDetailed(this, "Unable to enable metrics.", e, statistisc); + reporter.reportDetailed(this, Report.newBuilder(REPORT_METRICS_IO_ERROR).error(e).callerParam(statistisc)); } catch (Throwable e) { - reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc); + reporter.reportDetailed(this, Report.newBuilder(REPORT_METRICS_GENERIC_ERROR).error(e).callerParam(statistisc)); } } @@ -308,7 +328,7 @@ public class ProtocolLibrary extends JavaPlugin { return current; } catch (Exception e) { - reporter.reportWarning(this, "Unable to retrieve current Minecraft version.", e); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PARSE_MINECRAFT_VERSION).error(e)); } // Unknown version @@ -345,7 +365,7 @@ public class ProtocolLibrary extends JavaPlugin { } } catch (Exception e) { - reporter.reportWarning(this, "Unable to detect conflicting plugin versions.", e); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS).error(e)); } // See if the newest version is actually higher @@ -374,7 +394,9 @@ public class ProtocolLibrary extends JavaPlugin { throw new RuntimeException("plugin.yml might be corrupt."); } catch (RuntimeException e) { - reporter.reportWarning(this, "Cannot register command " + name + ": " + e.getMessage()); + reporter.reportWarning(this, + Report.newBuilder(REPORT_CANNOT_REGISTER_COMMAND).messageParam(name, e.getMessage()).error(e) + ); } } @@ -408,7 +430,7 @@ public class ProtocolLibrary extends JavaPlugin { } catch (Throwable e) { if (asyncPacketTask == -1) { - reporter.reportDetailed(this, "Unable to create packet timeout task.", e); + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CREATE_TIMEOUT_TASK).error(e)); } } } @@ -431,7 +453,7 @@ public class ProtocolLibrary extends JavaPlugin { commandProtocol.updateFinished(); } } catch (Exception e) { - reporter.reportDetailed(this, "Cannot perform automatic updates.", e); + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e)); updateDisabled = true; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java index 09786d1a..80ba3d37 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java @@ -1,387 +1,431 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.error; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; - -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.reflect.PrettyPrinter; -import com.google.common.primitives.Primitives; - -/** - * Internal class used to handle exceptions. - * - * @author Kristian - */ -public class DetailedErrorReporter implements ErrorReporter { - - public static final String SECOND_LEVEL_PREFIX = " "; - public static final String DEFAULT_PREFIX = " "; - public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/"; - public static final String PLUGIN_NAME = "ProtocolLib"; - - // Users that are informed about errors in the chat - public static final String ERROR_PERMISSION = "protocol.info"; - - // We don't want to spam the server - public static final int DEFAULT_MAX_ERROR_COUNT = 20; - - // Prevent spam per plugin too - private ConcurrentMap warningCount = new ConcurrentHashMap(); - - protected String prefix; - protected String supportURL; - - protected AtomicInteger internalErrorCount = new AtomicInteger(); - - protected int maxErrorCount; - protected Logger logger; - - protected WeakReference pluginReference; - - // Whether or not Apache Commons is not present - protected boolean apacheCommonsMissing; - - // Map of global objects - protected Map globalParameters = new HashMap(); - - /** - * Create a default error reporting system. - */ - public DetailedErrorReporter(Plugin plugin) { - this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL); - } - - /** - * Create a central error reporting system. - * @param plugin - the plugin owner. - * @param prefix - default line prefix. - * @param supportURL - URL to report the error. - */ - public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) { - this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger()); - } - - // Attempt to get the logger. - private static Logger getBukkitLogger() { - try { - return Bukkit.getLogger(); - } catch (Throwable e) { - return Logger.getLogger("Minecraft"); - } - } - - /** - * Create a central error reporting system. - * @param plugin - the plugin owner. - * @param prefix - default line prefix. - * @param supportURL - URL to report the error. - * @param maxErrorCount - number of errors to print before giving up. - * @param logger - current logger. - */ - public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) { - if (plugin == null) - throw new IllegalArgumentException("Plugin cannot be NULL."); - - this.pluginReference = new WeakReference(plugin); - this.prefix = prefix; - this.supportURL = supportURL; - this.maxErrorCount = maxErrorCount; - this.logger = logger; - } - - @Override - public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) { - if (reportMinimalNoSpam(sender, methodName, error)) { - // Print parameters, if they are given - if (parameters != null && parameters.length > 0) { - logger.log(Level.SEVERE, " Parameters:"); - - // Print each parameter - for (Object parameter : parameters) { - logger.log(Level.SEVERE, " " + getStringDescription(parameter)); - } - } - } - } - - @Override - public void reportMinimal(Plugin sender, String methodName, Throwable error) { - reportMinimalNoSpam(sender, methodName, error); - } - - public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) { - String pluginName = PacketAdapter.getPluginName(sender); - AtomicInteger counter = warningCount.get(pluginName); - - // Thread safe pattern - if (counter == null) { - AtomicInteger created = new AtomicInteger(); - counter = warningCount.putIfAbsent(pluginName, created); - - if (counter == null) { - counter = created; - } - } - - final int errorCount = counter.incrementAndGet(); - - // See if we should print the full error - if (errorCount < getMaxErrorCount()) { - logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception occured in " + - methodName + " for " + pluginName, error); - return true; - - } else { - // Nope - only print the error count occationally - if (isPowerOfTwo(errorCount)) { - logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception number " + errorCount + " occured in " + - methodName + " for " + pluginName, error); - } - return false; - } - } - - /** - * Determine if a given number is a power of two. - *

- * That is, if there exists an N such that 2^N = number. - * @param number - the number to check. - * @return TRUE if the given number is a power of two, FALSE otherwise. - */ - private boolean isPowerOfTwo(int number) { - return (number & (number - 1)) == 0; - } - - @Override - public void reportWarning(Object sender, String message) { - logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message); - } - - @Override - public void reportWarning(Object sender, String message, Throwable error) { - logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message, error); - } - - private String getSenderName(Object sender) { - if (sender != null) - return sender.getClass().getSimpleName(); - else - return "NULL"; - } - - @Override - public void reportDetailed(Object sender, String message, Throwable error, Object... parameters) { - - final Plugin plugin = pluginReference.get(); - final int errorCount = internalErrorCount.incrementAndGet(); - - // Do not overtly spam the server! - if (errorCount > getMaxErrorCount()) { - // Only allow the error count at rare occations - if (isPowerOfTwo(errorCount)) { - // Permit it - but print the number of exceptions first - reportWarning(this, "Internal exception count: " + errorCount + "!"); - } else { - // NEVER SPAM THE CONSOLE - return; - } - } - - StringWriter text = new StringWriter(); - PrintWriter writer = new PrintWriter(text); - - // Helpful message - writer.println("[ProtocolLib] INTERNAL ERROR: " + message); - writer.println("If this problem hasn't already been reported, please open a ticket"); - writer.println("at " + supportURL + " with the following data:"); - - // Now, let us print important exception information - writer.println(" ===== STACK TRACE ====="); - - if (error != null) - error.printStackTrace(writer); - - // Data dump! - writer.println(" ===== DUMP ====="); - - // Relevant parameters - if (parameters != null && parameters.length > 0) { - writer.println("Parameters:"); - - // We *really* want to get as much information as possible - for (Object param : parameters) { - writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX)); - } - } - - // Global parameters - for (String param : globalParameters()) { - writer.println(SECOND_LEVEL_PREFIX + param + ":"); - writer.println(addPrefix(getStringDescription(getGlobalParameter(param)), - SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX)); - } - - // Now, for the sender itself - writer.println("Sender:"); - writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX)); - - // And plugin - if (plugin != null) { - writer.println("Version:"); - writer.println(addPrefix(plugin.toString(), SECOND_LEVEL_PREFIX)); - } - - // Add the server version too - if (Bukkit.getServer() != null) { - writer.println("Server:"); - writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX)); - - // Inform of this occurrence - if (ERROR_PERMISSION != null) { - Bukkit.getServer().broadcast( - String.format("Error %s (%s) occured in %s.", message, error, sender), - ERROR_PERMISSION - ); - } - } - - // Make sure it is reported - logger.severe(addPrefix(text.toString(), prefix)); - } - - /** - * Adds the given prefix to every line in the text. - * @param text - text to modify. - * @param prefix - prefix added to every line in the text. - * @return The modified text. - */ - protected String addPrefix(String text, String prefix) { - return text.replaceAll("(?m)^", prefix); - } - - protected String getStringDescription(Object value) { - - // We can't only rely on toString. - if (value == null) { - return "[NULL]"; - } if (isSimpleType(value)) { - return value.toString(); - } else { - try { - if (!apacheCommonsMissing) - return (ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null)); - } catch (Throwable ex) { - // Apache is probably missing - apacheCommonsMissing = true; - } - - // Use our custom object printer instead - try { - return PrettyPrinter.printObject(value, value.getClass(), Object.class); - } catch (IllegalAccessException e) { - return "[Error: " + e.getMessage() + "]"; - } - } - } - - /** - * Determine if the given object is a wrapper for a primitive/simple type or not. - * @param test - the object to test. - * @return TRUE if this object is simple enough to simply be printed, FALSE othewise. - */ - protected boolean isSimpleType(Object test) { - return test instanceof String || Primitives.isWrapperType(test.getClass()); - } - - public int getErrorCount() { - return internalErrorCount.get(); - } - - public void setErrorCount(int errorCount) { - internalErrorCount.set(errorCount); - } - - public int getMaxErrorCount() { - return maxErrorCount; - } - - public void setMaxErrorCount(int maxErrorCount) { - this.maxErrorCount = maxErrorCount; - } - - /** - * Adds the given global parameter. It will be included in every error report. - * @param key - name of parameter. - * @param value - the global parameter itself. - */ - public void addGlobalParameter(String key, Object value) { - globalParameters.put(key, value); - } - - public Object getGlobalParameter(String key) { - return globalParameters.get(key); - } - - public void clearGlobalParameters() { - globalParameters.clear(); - } - - public Set globalParameters() { - return globalParameters.keySet(); - } - - public String getSupportURL() { - return supportURL; - } - - public void setSupportURL(String supportURL) { - this.supportURL = supportURL; - } - - public String getPrefix() { - return prefix; - } - - public void setPrefix(String prefix) { - this.prefix = prefix; - } - - public Logger getLogger() { - return logger; - } - - public void setLogger(Logger logger) { - this.logger = logger; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.error; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.Report.ReportBuilder; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.reflect.PrettyPrinter; +import com.google.common.primitives.Primitives; + +/** + * Internal class used to handle exceptions. + * + * @author Kristian + */ +public class DetailedErrorReporter implements ErrorReporter { + /** + * Report format for printing the current exception count. + */ + public static final ReportType REPORT_EXCEPTION_COUNT = new ReportType("Internal exception count: %s!"); + + public static final String SECOND_LEVEL_PREFIX = " "; + public static final String DEFAULT_PREFIX = " "; + public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/"; + + // Users that are informed about errors in the chat + public static final String ERROR_PERMISSION = "protocol.info"; + + // We don't want to spam the server + public static final int DEFAULT_MAX_ERROR_COUNT = 20; + + // Prevent spam per plugin too + private ConcurrentMap warningCount = new ConcurrentHashMap(); + + protected String prefix; + protected String supportURL; + + protected AtomicInteger internalErrorCount = new AtomicInteger(); + + protected int maxErrorCount; + protected Logger logger; + + protected WeakReference pluginReference; + protected String pluginName; + + // Whether or not Apache Commons is not present + protected boolean apacheCommonsMissing; + + // Map of global objects + protected Map globalParameters = new HashMap(); + + /** + * Create a default error reporting system. + */ + public DetailedErrorReporter(Plugin plugin) { + this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL); + } + + /** + * Create a central error reporting system. + * @param plugin - the plugin owner. + * @param prefix - default line prefix. + * @param supportURL - URL to report the error. + */ + public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) { + this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger()); + } + + /** + * Create a central error reporting system. + * @param plugin - the plugin owner. + * @param prefix - default line prefix. + * @param supportURL - URL to report the error. + * @param maxErrorCount - number of errors to print before giving up. + * @param logger - current logger. + */ + public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) { + if (plugin == null) + throw new IllegalArgumentException("Plugin cannot be NULL."); + + this.pluginReference = new WeakReference(plugin); + this.pluginName = plugin.getName(); + this.prefix = prefix; + this.supportURL = supportURL; + this.maxErrorCount = maxErrorCount; + this.logger = logger; + } + + // Attempt to get the logger. + private static Logger getBukkitLogger() { + try { + return Bukkit.getLogger(); + } catch (Throwable e) { + return Logger.getLogger("Minecraft"); + } + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) { + if (reportMinimalNoSpam(sender, methodName, error)) { + // Print parameters, if they are given + if (parameters != null && parameters.length > 0) { + logger.log(Level.SEVERE, printParameters(parameters)); + } + } + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error) { + reportMinimalNoSpam(sender, methodName, error); + } + + public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) { + String pluginName = PacketAdapter.getPluginName(sender); + AtomicInteger counter = warningCount.get(pluginName); + + // Thread safe pattern + if (counter == null) { + AtomicInteger created = new AtomicInteger(); + counter = warningCount.putIfAbsent(pluginName, created); + + if (counter == null) { + counter = created; + } + } + + final int errorCount = counter.incrementAndGet(); + + // See if we should print the full error + if (errorCount < getMaxErrorCount()) { + logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception occured in " + + methodName + " for " + pluginName, error); + return true; + + } else { + // Nope - only print the error count occationally + if (isPowerOfTwo(errorCount)) { + logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception number " + errorCount + " occured in " + + methodName + " for " + pluginName, error); + } + return false; + } + } + + /** + * Determine if a given number is a power of two. + *

+ * That is, if there exists an N such that 2^N = number. + * @param number - the number to check. + * @return TRUE if the given number is a power of two, FALSE otherwise. + */ + private boolean isPowerOfTwo(int number) { + return (number & (number - 1)) == 0; + } + + @Override + public void reportWarning(Object sender, ReportBuilder reportBuilder) { + if (reportBuilder == null) + throw new IllegalArgumentException("reportBuilder cannot be NULL."); + + reportWarning(sender, reportBuilder.build()); + } + + @Override + public void reportWarning(Object sender, Report report) { + String message = "[" + pluginName + "] [" + getSenderName(sender) + "] " + report.getReportMessage(); + + // Print the main warning + if (report.getException() != null) { + logger.log(Level.WARNING, message, report.getException()); + } else { + logger.log(Level.WARNING, message); + } + + // Parameters? + if (report.hasCallerParameters()) { + // Write it + logger.log(Level.WARNING, printParameters(report.getCallerParameters())); + } + } + + /** + * Retrieve the name of a sender class. + * @param sender - sender object. + * @return The name of the sender's class. + */ + private String getSenderName(Object sender) { + if (sender != null) + return sender.getClass().getSimpleName(); + else + return "NULL"; + } + + @Override + public void reportDetailed(Object sender, ReportBuilder reportBuilder) { + reportDetailed(sender, reportBuilder.build()); + } + + @Override + public void reportDetailed(Object sender, Report report) { + final Plugin plugin = pluginReference.get(); + final int errorCount = internalErrorCount.incrementAndGet(); + + // Do not overtly spam the server! + if (errorCount > getMaxErrorCount()) { + // Only allow the error count at rare occations + if (isPowerOfTwo(errorCount)) { + // Permit it - but print the number of exceptions first + reportWarning(this, Report.newBuilder(REPORT_EXCEPTION_COUNT).messageParam(errorCount).build()); + } else { + // NEVER SPAM THE CONSOLE + return; + } + } + + StringWriter text = new StringWriter(); + PrintWriter writer = new PrintWriter(text); + + // Helpful message + writer.println("[" + pluginName + "] INTERNAL ERROR: " + report.getReportMessage()); + writer.println("If this problem hasn't already been reported, please open a ticket"); + writer.println("at " + supportURL + " with the following data:"); + + // Now, let us print important exception information + writer.println(" ===== STACK TRACE ====="); + + if (report.getException() != null) { + report.getException().printStackTrace(writer); + } + + // Data dump! + writer.println(" ===== DUMP ====="); + + // Relevant parameters + if (report.hasCallerParameters()) { + printParameters(writer, report.getCallerParameters()); + } + + // Global parameters + for (String param : globalParameters()) { + writer.println(SECOND_LEVEL_PREFIX + param + ":"); + writer.println(addPrefix(getStringDescription(getGlobalParameter(param)), + SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX)); + } + + // Now, for the sender itself + writer.println("Sender:"); + writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX)); + + // And plugin + if (plugin != null) { + writer.println("Version:"); + writer.println(addPrefix(plugin.toString(), SECOND_LEVEL_PREFIX)); + } + + // Add the server version too + if (Bukkit.getServer() != null) { + writer.println("Server:"); + writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX)); + + // Inform of this occurrence + if (ERROR_PERMISSION != null) { + Bukkit.getServer().broadcast( + String.format("Error %s (%s) occured in %s.", report.getReportMessage(), report.getException(), sender), + ERROR_PERMISSION + ); + } + } + + // Make sure it is reported + logger.severe(addPrefix(text.toString(), prefix)); + } + + private String printParameters(Object... parameters) { + StringWriter writer = new StringWriter(); + + // Print and retrieve the string buffer + printParameters(new PrintWriter(writer), parameters); + return writer.toString(); + } + + private void printParameters(PrintWriter writer, Object[] parameters) { + writer.println("Parameters: "); + + // We *really* want to get as much information as possible + for (Object param : parameters) { + writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX)); + } + } + + /** + * Adds the given prefix to every line in the text. + * @param text - text to modify. + * @param prefix - prefix added to every line in the text. + * @return The modified text. + */ + protected String addPrefix(String text, String prefix) { + return text.replaceAll("(?m)^", prefix); + } + + /** + * Retrieve a string representation of the given object. + * @param value - object to convert. + * @return String representation. + */ + protected String getStringDescription(Object value) { + + // We can't only rely on toString. + if (value == null) { + return "[NULL]"; + } if (isSimpleType(value)) { + return value.toString(); + } else { + try { + if (!apacheCommonsMissing) + return (ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null)); + } catch (Throwable ex) { + // Apache is probably missing + apacheCommonsMissing = true; + } + + // Use our custom object printer instead + try { + return PrettyPrinter.printObject(value, value.getClass(), Object.class); + } catch (IllegalAccessException e) { + return "[Error: " + e.getMessage() + "]"; + } + } + } + + /** + * Determine if the given object is a wrapper for a primitive/simple type or not. + * @param test - the object to test. + * @return TRUE if this object is simple enough to simply be printed, FALSE othewise. + */ + protected boolean isSimpleType(Object test) { + return test instanceof String || Primitives.isWrapperType(test.getClass()); + } + + public int getErrorCount() { + return internalErrorCount.get(); + } + + public void setErrorCount(int errorCount) { + internalErrorCount.set(errorCount); + } + + public int getMaxErrorCount() { + return maxErrorCount; + } + + public void setMaxErrorCount(int maxErrorCount) { + this.maxErrorCount = maxErrorCount; + } + + /** + * Adds the given global parameter. It will be included in every error report. + * @param key - name of parameter. + * @param value - the global parameter itself. + */ + public void addGlobalParameter(String key, Object value) { + globalParameters.put(key, value); + } + + public Object getGlobalParameter(String key) { + return globalParameters.get(key); + } + + public void clearGlobalParameters() { + globalParameters.clear(); + } + + public Set globalParameters() { + return globalParameters.keySet(); + } + + public String getSupportURL() { + return supportURL; + } + + public void setSupportURL(String supportURL) { + this.supportURL = supportURL; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public Logger getLogger() { + return logger; + } + + public void setLogger(Logger logger) { + this.logger = logger; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java index e1b7f927..7d45cdde 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java @@ -1,65 +1,69 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.error; - -import org.bukkit.plugin.Plugin; - -public interface ErrorReporter { - - /** - * Prints a small minimal error report about an exception from another plugin. - * @param sender - the other plugin. - * @param methodName - name of the caller method. - * @param error - the exception itself. - */ - public abstract void reportMinimal(Plugin sender, String methodName, Throwable error); - - /** - * Prints a small minimal error report about an exception from another plugin. - * @param sender - the other plugin. - * @param methodName - name of the caller method. - * @param error - the exception itself. - * @param parameters - any relevant parameters to print. - */ - public abstract void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters); - - /** - * Prints a warning message from the current plugin. - * @param sender - the object containing the caller method. - * @param message - error message. - */ - public abstract void reportWarning(Object sender, String message); - - /** - * Prints a warning message from the current plugin. - * @param sender - the object containing the caller method. - * @param message - error message. - * @param error - the exception that was thrown. - */ - public abstract void reportWarning(Object sender, String message, Throwable error); - - /** - * Prints a detailed error report about an unhandled exception. - * @param sender - the object containing the caller method. - * @param message - an error message to include. - * @param error - the exception that was thrown in the caller method. - * @param parameters - parameters from the caller method. - */ - public abstract void reportDetailed(Object sender, String message, Throwable error, Object... parameters); - +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.error; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.Report.ReportBuilder; + +public interface ErrorReporter { + /** + * Prints a small minimal error report about an exception from another plugin. + * @param sender - the other plugin. + * @param methodName - name of the caller method. + * @param error - the exception itself. + */ + public abstract void reportMinimal(Plugin sender, String methodName, Throwable error); + + /** + * Prints a small minimal error report about an exception from another plugin. + * @param sender - the other plugin. + * @param methodName - name of the caller method. + * @param error - the exception itself. + * @param parameters - any relevant parameters to print. + */ + public abstract void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters); + + /** + * Prints a warning message from the current plugin. + * @param sender - the object containing the caller method. + * @param report - an error report to include. + */ + public abstract void reportWarning(Object sender, Report report); + + /** + * Prints a warning message from the current plugin. + * @param sender - the object containing the caller method. + * @param reportBuilder - an error report builder that will be used to get the report. + */ + public abstract void reportWarning(Object sender, ReportBuilder reportBuilder); + + /** + * Prints a detailed error report about an unhandled exception. + * @param sender - the object containing the caller method. + * @param report - an error report to include. + */ + public abstract void reportDetailed(Object sender, Report report); + + /** + * Prints a detailed error report about an unhandled exception. + * @param sender - the object containing the caller method. + * @param reportBuilder - an error report builder that will be used to get the report. + */ + public abstract void reportDetailed(Object sender, ReportBuilder reportBuilder); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java new file mode 100644 index 00000000..177c0407 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java @@ -0,0 +1,162 @@ +package com.comphenix.protocol.error; + +import javax.annotation.Nullable; + +/** + * Represents a error or warning report. + * + * @author Kristian + */ +public class Report { + private final ReportType type; + private final Throwable exception; + private final Object[] messageParameters; + private final Object[] callerParameters; + + /** + * Must be constructed through the factory method in Report. + */ + public static class ReportBuilder { + private ReportType type; + private Throwable exception; + private Object[] messageParameters; + private Object[] callerParameters; + + private ReportBuilder() { + // Don't allow + } + + /** + * Set the current report type. Cannot be NULL. + * @param type - report type. + * @return This builder, for chaining. + */ + public ReportBuilder type(ReportType type) { + if (type == null) + throw new IllegalArgumentException("Report type cannot be set to NULL."); + this.type = type; + return this; + } + + /** + * Set the current exception that occured. + * @param exception - exception that occured. + * @return This builder, for chaining. + */ + public ReportBuilder error(@Nullable Throwable exception) { + this.exception = exception; + return this; + } + + /** + * Set the message parameters that are used to construct a message text. + * @param messageParameters - parameters for the report type. + * @return This builder, for chaining. + */ + public ReportBuilder messageParam(@Nullable Object... messageParameters) { + this.messageParameters = messageParameters; + return this; + } + + /** + * Set the parameters in the caller method. This is optional. + * @param callerParameters - parameters of the caller method. + * @return This builder, for chaining. + */ + public ReportBuilder callerParam(@Nullable Object... callerParameters) { + this.callerParameters = callerParameters; + return this; + } + + /** + * Construct a new report with the provided input. + * @return A new report. + */ + public Report build() { + return new Report(type, exception, messageParameters, callerParameters); + } + } + + /** + * Construct a new report builder. + * @param type - the initial report type. + * @return Report builder. + */ + public static ReportBuilder newBuilder(ReportType type) { + return new ReportBuilder().type(type); + } + + /** + * Construct a new report with the given type and parameters. + * @param exception - exception that occured in the caller method. + * @param type - the report type that will be used to construct the message. + * @param messageParameters - parameters used to construct the report message. + * @param callerParameters - parameters from the caller method. + */ + protected Report(ReportType type, @Nullable Throwable exception, @Nullable Object[] messageParameters, @Nullable Object[] callerParameters) { + if (type == null) + throw new IllegalArgumentException("type cannot be NULL."); + this.type = type; + this.exception = exception; + this.messageParameters = messageParameters; + this.callerParameters = callerParameters; + } + + /** + * Format the current report type with the provided message parameters. + * @return The formated report message. + */ + public String getReportMessage() { + return type.getMessage(messageParameters); + } + + /** + * Retrieve the message parameters that will be used to construc the report message. + * 0; + } + + /** + * Determine if we have any caller parameters. + * @return TRUE if there are any caller parameters, FALSE otherwise. + */ + public boolean hasCallerParameters() { + return callerParameters != null && callerParameters.length > 0; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java new file mode 100644 index 00000000..cd4490e0 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java @@ -0,0 +1,66 @@ +package com.comphenix.protocol.error; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Represents a strongly-typed report. Subclasses should be immutable. + *

+ * By convention, a report must be declared as a static field publicly accessible from the sender class. + * @author Kristian + */ +public class ReportType { + private final String errorFormat; + + /** + * Construct a new report type. + * @param errorFormat - string used to format the underlying report. + */ + public ReportType(String errorFormat) { + this.errorFormat = errorFormat; + } + + /** + * Convert the given report to a string, using the provided parameters. + * @param parameters - parameters to insert, or NULL to insert nothing. + * @return The full report in string format. + */ + public String getMessage(Object[] parameters) { + if (parameters == null || parameters.length == 0) + return toString(); + else + return String.format(errorFormat, parameters); + } + + @Override + public String toString() { + return errorFormat; + } + + /** + * Retrieve all publicly associated reports. + * @param clazz - sender class. + * @return All associated reports. + */ + public static ReportType[] getReports(Class clazz) { + if (clazz == null) + throw new IllegalArgumentException("clazz cannot be NULL."); + List result = new ArrayList(); + + for (Field field : clazz.getFields()) { + if (Modifier.isStatic(field.getModifiers()) && + ReportType.class.isAssignableFrom(field.getDeclaringClass())) { + try { + result.add((ReportType) field.get(null)); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Unable to access field.", e); + } + } + } + return result.toArray(new ReportType[0]); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java index ae444147..7a1a724d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java @@ -1,181 +1,213 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.injector.PacketConstructor.Unwrapper; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.instances.DefaultInstances; -import com.google.common.primitives.Primitives; - -/** - * Represents an object capable of converting wrapped Bukkit objects into NMS objects. - *

- * Typical conversions include: - *

    - *
  • org.bukkit.entity.Player -> net.minecraft.server.EntityPlayer
  • - *
  • org.bukkit.World -> net.minecraft.server.WorldServer
  • - *
- * - * @author Kristian - */ -public class BukkitUnwrapper implements Unwrapper { - private static Map, Unwrapper> unwrapperCache = new ConcurrentHashMap, Unwrapper>(); - - @SuppressWarnings("unchecked") - @Override - public Object unwrapItem(Object wrappedObject) { - // Special case - if (wrappedObject == null) - return null; - Class currentClass = wrappedObject.getClass(); - - // Next, check for types that doesn't have a getHandle() - if (wrappedObject instanceof Collection) { - return handleCollection((Collection) wrappedObject); - } else if (Primitives.isWrapperType(currentClass) || wrappedObject instanceof String) { - return null; - } - - Unwrapper specificUnwrapper = getSpecificUnwrapper(currentClass); - - // Retrieve the handle - if (specificUnwrapper != null) - return specificUnwrapper.unwrapItem(wrappedObject); - else - return null; - } - - // Handle a collection of items - private Object handleCollection(Collection wrappedObject) { - - @SuppressWarnings("unchecked") - Collection copy = DefaultInstances.DEFAULT.getDefault(wrappedObject.getClass()); - - if (copy != null) { - // Unwrap every element - for (Object element : wrappedObject) { - copy.add(unwrapItem(element)); - } - return copy; - - } else { - // Impossible - return null; - } - } - - /** - * Retrieve a cached class unwrapper for the given class. - * @param type - the type of the class. - * @return An unwrapper for the given class. - */ - private Unwrapper getSpecificUnwrapper(Class type) { - // See if we're already determined this - if (unwrapperCache.containsKey(type)) { - // We will never remove from the cache, so this ought to be thread safe - return unwrapperCache.get(type); - } - - try { - final Method find = type.getMethod("getHandle"); - - // It's thread safe, as getMethod should return the same handle - Unwrapper methodUnwrapper = new Unwrapper() { - @Override - public Object unwrapItem(Object wrappedObject) { - - try { - return find.invoke(wrappedObject); - - } catch (IllegalArgumentException e) { - ProtocolLibrary.getErrorReporter().reportDetailed( - this, "Illegal argument.", e, wrappedObject, find); - } catch (IllegalAccessException e) { - // Should not occur either - return null; - } catch (InvocationTargetException e) { - // This is really bad - throw new RuntimeException("Minecraft error.", e); - } - - return null; - } - }; - - unwrapperCache.put(type, methodUnwrapper); - return methodUnwrapper; - - } catch (SecurityException e) { - ProtocolLibrary.getErrorReporter().reportDetailed(this, "Security limitation.", e, type.getName()); - } catch (NoSuchMethodException e) { - // Try getting the field unwrapper too - Unwrapper fieldUnwrapper = getFieldUnwrapper(type); - - if (fieldUnwrapper != null) - return fieldUnwrapper; - else - ProtocolLibrary.getErrorReporter().reportDetailed(this, "Cannot find method.", e, type.getName()); - } - - // Default method - return null; - } - - /** - * Retrieve a cached unwrapper using the handle field. - * @param type - a cached field unwrapper. - * @return The cached field unwrapper. - */ - private Unwrapper getFieldUnwrapper(Class type) { - final Field find = FieldUtils.getField(type, "handle", true); - - // See if we succeeded - if (find != null) { - Unwrapper fieldUnwrapper = new Unwrapper() { - @Override - public Object unwrapItem(Object wrappedObject) { - try { - return FieldUtils.readField(find, wrappedObject, true); - } catch (IllegalAccessException e) { - ProtocolLibrary.getErrorReporter().reportDetailed( - this, "Cannot read field 'handle'.", e, wrappedObject, find.getName()); - return null; - } - } - }; - - unwrapperCache.put(type, fieldUnwrapper); - return fieldUnwrapper; - - } else { - // Inform about this too - ProtocolLibrary.getErrorReporter().reportDetailed( - this, "Could not find field 'handle'.", - new Exception("Unable to find 'handle'"), type.getName()); - return null; - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.injector.PacketConstructor.Unwrapper; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.google.common.primitives.Primitives; + +/** + * Represents an object capable of converting wrapped Bukkit objects into NMS objects. + *

+ * Typical conversions include: + *

    + *
  • org.bukkit.entity.Player -> net.minecraft.server.EntityPlayer
  • + *
  • org.bukkit.World -> net.minecraft.server.WorldServer
  • + *
+ * + * @author Kristian + */ +public class BukkitUnwrapper implements Unwrapper { + public static final ReportType REPORT_ILLEGAL_ARGUMENT = new ReportType("Illegal argument."); + public static final ReportType REPORT_SECURITY_LIMITATION = new ReportType("Security limitation."); + public static final ReportType REPORT_CANNOT_FIND_UNWRAP_METHOD = new ReportType("Cannot find method."); + + public static final ReportType REPORT_CANNOT_READ_FIELD_HANDLE = new ReportType("Cannot read field 'handle'."); + + private static Map, Unwrapper> unwrapperCache = new ConcurrentHashMap, Unwrapper>(); + + // The current error reporter + private final ErrorReporter reporter; + + /** + * Construct a new Bukkit unwrapper with ProtocolLib's default error reporter. + */ + public BukkitUnwrapper() { + this(ProtocolLibrary.getErrorReporter()); + } + + /** + * Construct a new Bukkit unwrapper with the given error reporter. + * @param reporter - the error reporter to use. + */ + public BukkitUnwrapper(ErrorReporter reporter) { + this.reporter = reporter; + } + + @SuppressWarnings("unchecked") + @Override + public Object unwrapItem(Object wrappedObject) { + // Special case + if (wrappedObject == null) + return null; + Class currentClass = wrappedObject.getClass(); + + // Next, check for types that doesn't have a getHandle() + if (wrappedObject instanceof Collection) { + return handleCollection((Collection) wrappedObject); + } else if (Primitives.isWrapperType(currentClass) || wrappedObject instanceof String) { + return null; + } + + Unwrapper specificUnwrapper = getSpecificUnwrapper(currentClass); + + // Retrieve the handle + if (specificUnwrapper != null) + return specificUnwrapper.unwrapItem(wrappedObject); + else + return null; + } + + // Handle a collection of items + private Object handleCollection(Collection wrappedObject) { + + @SuppressWarnings("unchecked") + Collection copy = DefaultInstances.DEFAULT.getDefault(wrappedObject.getClass()); + + if (copy != null) { + // Unwrap every element + for (Object element : wrappedObject) { + copy.add(unwrapItem(element)); + } + return copy; + + } else { + // Impossible + return null; + } + } + + /** + * Retrieve a cached class unwrapper for the given class. + * @param type - the type of the class. + * @return An unwrapper for the given class. + */ + private Unwrapper getSpecificUnwrapper(Class type) { + // See if we're already determined this + if (unwrapperCache.containsKey(type)) { + // We will never remove from the cache, so this ought to be thread safe + return unwrapperCache.get(type); + } + + try { + final Method find = type.getMethod("getHandle"); + + // It's thread safe, as getMethod should return the same handle + Unwrapper methodUnwrapper = new Unwrapper() { + @Override + public Object unwrapItem(Object wrappedObject) { + + try { + return find.invoke(wrappedObject); + + } catch (IllegalArgumentException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_ILLEGAL_ARGUMENT).error(e).callerParam(wrappedObject, find) + ); + } catch (IllegalAccessException e) { + // Should not occur either + return null; + } catch (InvocationTargetException e) { + // This is really bad + throw new RuntimeException("Minecraft error.", e); + } + + return null; + } + }; + + unwrapperCache.put(type, methodUnwrapper); + return methodUnwrapper; + + } catch (SecurityException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_SECURITY_LIMITATION).error(e).callerParam(type) + ); + } catch (NoSuchMethodException e) { + // Try getting the field unwrapper too + Unwrapper fieldUnwrapper = getFieldUnwrapper(type); + + if (fieldUnwrapper != null) + return fieldUnwrapper; + else + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_FIND_UNWRAP_METHOD).error(e).callerParam(type)); + } + + // Default method + return null; + } + + /** + * Retrieve a cached unwrapper using the handle field. + * @param type - a cached field unwrapper. + * @return The cached field unwrapper. + */ + private Unwrapper getFieldUnwrapper(Class type) { + final Field find = FieldUtils.getField(type, "handle", true); + + // See if we succeeded + if (find != null) { + Unwrapper fieldUnwrapper = new Unwrapper() { + @Override + public Object unwrapItem(Object wrappedObject) { + try { + return FieldUtils.readField(find, wrappedObject, true); + } catch (IllegalAccessException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_READ_FIELD_HANDLE).error(e).callerParam(wrappedObject, find) + ); + return null; + } + } + }; + + unwrapperCache.put(type, fieldUnwrapper); + return fieldUnwrapper; + + } else { + // Inform about this too + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_READ_FIELD_HANDLE).callerParam(find) + ); + return null; + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 6aa6300b..88b00209 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -50,6 +50,8 @@ import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.async.AsyncMarker; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.events.*; import com.comphenix.protocol.injector.packet.PacketInjector; import com.comphenix.protocol.injector.packet.PacketInjectorBuilder; @@ -67,7 +69,23 @@ import com.google.common.base.Predicate; import com.google.common.collect.ImmutableSet; public final class PacketFilterManager implements ProtocolManager, ListenerInvoker { + public static final ReportType REPORT_CANNOT_LOAD_PACKET_LIST = new ReportType("Cannot load server and client packet list."); + public static final ReportType REPORT_CANNOT_INITIALIZE_PACKET_INJECTOR = new ReportType("Unable to initialize packet injector"); + + public static final ReportType REPORT_PLUGIN_DEPEND_MISSING = + new ReportType("%s doesn't depend on ProtocolLib. Check that its plugin.yml has a 'depend' directive."); + + // Registering packet IDs that are not supported + public static final ReportType REPORT_UNSUPPORTED_SERVER_PACKET_ID = new ReportType("[%s] Unsupported server packet ID in current Minecraft version: %s"); + public static final ReportType REPORT_UNSUPPORTED_CLIENT_PACKET_ID = new ReportType("[%s] Unsupported client packet ID in current Minecraft version: %s"); + + // Problems injecting and uninjecting players + public static final ReportType REPORT_CANNOT_UNINJECT_PLAYER = new ReportType("Unable to uninject net handler for player."); + public static final ReportType REPORT_CANNOT_UNINJECT_OFFLINE_PLAYER = new ReportType("Unable to uninject logged off player."); + public static final ReportType REPORT_CANNOT_INJECT_PLAYER = new ReportType("Unable to inject player."); + public static final ReportType REPORT_CANNOT_UNREGISTER_PLUGIN = new ReportType("Unable to handle disabled plugin."); + /** * Sets the inject hook type. Different types allow for maximum compatibility. * @author Kristian @@ -234,11 +252,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok knowsServerPackets = PacketRegistry.getServerPackets() != null; knowsClientPackets = PacketRegistry.getClientPackets() != null; } catch (FieldAccessException e) { - reporter.reportWarning(this, "Cannot load server and client packet list.", e); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_PACKET_LIST).error(e)); } } catch (FieldAccessException e) { - reporter.reportWarning(this, "Unable to initialize packet injector.", e); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_INITIALIZE_PACKET_INJECTOR).error(e)); } } @@ -282,7 +300,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok private void printPluginWarnings(Plugin plugin) { switch (pluginVerifier.verify(plugin)) { case NO_DEPEND: - reporter.reportWarning(this, plugin + " doesn't depend on ProtocolLib. Check that its plugin.yml has a 'depend' directive."); + reporter.reportWarning(this, Report.newBuilder(REPORT_PLUGIN_DEPEND_MISSING).messageParam(plugin.getName())); case VALID: // Do nothing break; @@ -510,10 +528,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (!knowsServerPackets || PacketRegistry.getServerPackets().contains(packetID)) playerInjection.addPacketHandler(packetID); else - reporter.reportWarning(this, String.format( - "[%s] Unsupported server packet ID in current Minecraft version: %s", - PacketAdapter.getPluginName(listener), packetID - )); + reporter.reportWarning(this, + Report.newBuilder(REPORT_UNSUPPORTED_SERVER_PACKET_ID).messageParam(PacketAdapter.getPluginName(listener), packetID) + ); } // As above, only for client packets @@ -521,10 +538,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (!knowsClientPackets || PacketRegistry.getClientPackets().contains(packetID)) packetInjector.addPacketHandler(packetID); else - reporter.reportWarning(this, String.format( - "[%s] Unsupported client packet ID in current Minecraft version: %s", - PacketAdapter.getPluginName(listener), packetID - )); + reporter.reportWarning(this, + Report.newBuilder(REPORT_UNSUPPORTED_CLIENT_PACKET_ID).messageParam(PacketAdapter.getPluginName(listener), packetID) + ); } } } @@ -722,7 +738,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok playerInjection.uninjectPlayer(event.getPlayer().getAddress()); playerInjection.updatePlayer(event.getPlayer()); } catch (Exception e) { - reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject net handler for player.", e, event); + reporter.reportDetailed(PacketFilterManager.this, + Report.newBuilder(REPORT_CANNOT_UNINJECT_PLAYER).callerParam(event).error(e) + ); } } @@ -731,7 +749,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // This call will be ignored if no listeners are registered playerInjection.injectPlayer(event.getPlayer(), ConflictStrategy.OVERRIDE); } catch (Exception e) { - reporter.reportDetailed(PacketFilterManager.this, "Unable to inject player.", e, event); + reporter.reportDetailed(PacketFilterManager.this, + Report.newBuilder(REPORT_CANNOT_INJECT_PLAYER).callerParam(event).error(e) + ); } } @@ -743,7 +763,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok playerInjection.handleDisconnect(player); playerInjection.uninjectPlayer(player); } catch (Exception e) { - reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject logged off player.", e, event); + reporter.reportDetailed(PacketFilterManager.this, + Report.newBuilder(REPORT_CANNOT_UNINJECT_OFFLINE_PLAYER).callerParam(event).error(e) + ); } } @@ -754,7 +776,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok removePacketListeners(event.getPlugin()); } } catch (Exception e) { - reporter.reportDetailed(PacketFilterManager.this, "Unable handle disabled plugin.", e, event); + reporter.reportDetailed(PacketFilterManager.this, + Report.newBuilder(REPORT_CANNOT_UNREGISTER_PLUGIN).callerParam(event).error(e) + ); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java index 0cfea2ed..72d70457 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java @@ -1,293 +1,303 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.packet; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import net.sf.cglib.proxy.Factory; - -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract; -import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; -import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.comphenix.protocol.wrappers.TroveWrapper; -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableSet; - -/** - * Static packet registry in Minecraft. - * - * @author Kristian - */ -@SuppressWarnings("rawtypes") -public class PacketRegistry { - private static final int MIN_SERVER_PACKETS = 5; - private static final int MIN_CLIENT_PACKETS = 5; - - // Fuzzy reflection - private static FuzzyReflection packetRegistry; - - // The packet class to packet ID translator - private static Map packetToID; - - // Whether or not certain packets are sent by the client or the server - private static ImmutableSet serverPackets; - private static ImmutableSet clientPackets; - - // The underlying sets - private static Set serverPacketsRef; - private static Set clientPacketsRef; - - // New proxy values - private static Map overwrittenPackets = new HashMap(); - - // Vanilla packets - private static Map previousValues = new HashMap(); - - @SuppressWarnings({ "unchecked" }) - public static Map getPacketToID() { - // Initialize it, if we haven't already - if (packetToID == null) { - try { - Field packetsField = getPacketRegistry().getFieldByType("packetsField", Map.class); - packetToID = (Map) FieldUtils.readStaticField(packetsField, true); - } catch (IllegalArgumentException e) { - // Spigot 1.2.5 MCPC workaround - try { - packetToID = getSpigotWrapper(); - } catch (Exception e2) { - // Very bad indeed - throw new IllegalArgumentException(e.getMessage() + "; Spigot workaround failed.", e2); - } - - } catch (IllegalAccessException e) { - throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e); - } - } - - return packetToID; - } - - private static Map getSpigotWrapper() throws IllegalAccessException { - // If it talks like a duck, etc. - // Perhaps it would be nice to have a proper duck typing library as well - FuzzyClassContract mapLike = FuzzyClassContract.newBuilder(). - method(FuzzyMethodContract.newBuilder(). - nameExact("size").returnTypeExact(int.class)). - method(FuzzyMethodContract.newBuilder(). - nameExact("put").parameterCount(2)). - method(FuzzyMethodContract.newBuilder(). - nameExact("get").parameterCount(1)). - build(); - - Field packetsField = getPacketRegistry().getField( - FuzzyFieldContract.newBuilder().typeMatches(mapLike).build()); - Object troveMap = FieldUtils.readStaticField(packetsField, true); - - // Check for stupid no_entry_values - try { - Field field = FieldUtils.getField(troveMap.getClass(), "no_entry_value", true); - Integer value = (Integer) FieldUtils.readField(field, troveMap, true); - - if (value >= 0 && value < 256) { - // Someone forgot to set the no entry value. Let's help them. - FieldUtils.writeField(field, troveMap, -1); - } - } catch (IllegalArgumentException e) { - // Whatever - ProtocolLibrary.getErrorReporter().reportWarning(PacketRegistry.class, "Unable to correct no entry value.", e); - } - - // We'll assume this a Trove map - return TroveWrapper.getDecoratedMap(troveMap); - } - - /** - * Retrieve the cached fuzzy reflection instance allowing access to the packet registry. - * @return Reflected packet registry. - */ - private static FuzzyReflection getPacketRegistry() { - if (packetRegistry == null) - packetRegistry = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true); - return packetRegistry; - } - - /** - * Retrieve the injected proxy classes handlig each packet ID. - * @return Injected classes. - */ - public static Map getOverwrittenPackets() { - return overwrittenPackets; - } - - /** - * Retrieve the vanilla classes handling each packet ID. - * @return Vanilla classes. - */ - public static Map getPreviousPackets() { - return previousValues; - } - - /** - * Retrieve every known and supported server packet. - * @return An immutable set of every known server packet. - * @throws FieldAccessException If we're unable to retrieve the server packet data from Minecraft. - */ - public static Set getServerPackets() throws FieldAccessException { - initializeSets(); - - // Sanity check. This is impossible! - if (serverPackets != null && serverPackets.size() < MIN_SERVER_PACKETS) - throw new FieldAccessException("Server packet list is empty. Seems to be unsupported"); - return serverPackets; - } - - /** - * Retrieve every known and supported client packet. - * @return An immutable set of every known client packet. - * @throws FieldAccessException If we're unable to retrieve the client packet data from Minecraft. - */ - public static Set getClientPackets() throws FieldAccessException { - initializeSets(); - - // As above - if (clientPackets != null && clientPackets.size() < MIN_CLIENT_PACKETS) - throw new FieldAccessException("Client packet list is empty. Seems to be unsupported"); - return clientPackets; - } - - @SuppressWarnings("unchecked") - private static void initializeSets() throws FieldAccessException { - if (serverPacketsRef == null || clientPacketsRef == null) { - List sets = getPacketRegistry().getFieldListByType(Set.class); - - try { - if (sets.size() > 1) { - serverPacketsRef = (Set) FieldUtils.readStaticField(sets.get(0), true); - clientPacketsRef = (Set) FieldUtils.readStaticField(sets.get(1), true); - - // Impossible - if (serverPacketsRef == null || clientPacketsRef == null) - throw new FieldAccessException("Packet sets are in an illegal state."); - - // NEVER allow callers to modify the underlying sets - serverPackets = ImmutableSet.copyOf(serverPacketsRef); - clientPackets = ImmutableSet.copyOf(clientPacketsRef); - - // Check sizes - if (serverPackets.size() < MIN_SERVER_PACKETS) - ProtocolLibrary.getErrorReporter().reportWarning( - PacketRegistry.class, "Too few server packets detected: " + serverPackets.size()); - if (clientPackets.size() < MIN_CLIENT_PACKETS) - ProtocolLibrary.getErrorReporter().reportWarning( - PacketRegistry.class, "Too few client packets detected: " + clientPackets.size()); - - } else { - throw new FieldAccessException("Cannot retrieve packet client/server sets."); - } - - } catch (IllegalAccessException e) { - throw new FieldAccessException("Cannot access field.", e); - } - - } else { - // Copy over again if it has changed - if (serverPacketsRef != null && serverPacketsRef.size() != serverPackets.size()) - serverPackets = ImmutableSet.copyOf(serverPacketsRef); - if (clientPacketsRef != null && clientPacketsRef.size() != clientPackets.size()) - clientPackets = ImmutableSet.copyOf(clientPacketsRef); - } - } - - /** - * Retrieves the correct packet class from a given packet ID. - * @param packetID - the packet ID. - * @return The associated class. - */ - public static Class getPacketClassFromID(int packetID) { - return getPacketClassFromID(packetID, false); - } - - /** - * Retrieves the correct packet class from a given packet ID. - * @param packetID - the packet ID. - * @param forceVanilla - whether or not to look for vanilla classes, not injected classes. - * @return The associated class. - */ - public static Class getPacketClassFromID(int packetID, boolean forceVanilla) { - - Map lookup = forceVanilla ? previousValues : overwrittenPackets; - - // Optimized lookup - if (lookup.containsKey(packetID)) { - return removeEnhancer(lookup.get(packetID), forceVanilla); - } - - // Will most likely not be used - for (Map.Entry entry : getPacketToID().entrySet()) { - if (Objects.equal(entry.getValue(), packetID)) { - // Attempt to get the vanilla class here too - if (!forceVanilla || MinecraftReflection.isMinecraftClass(entry.getKey())) - return removeEnhancer(entry.getKey(), forceVanilla); - } - } - - throw new IllegalArgumentException("The packet ID " + packetID + " is not registered."); - } - - /** - * Retrieve the packet ID of a given packet. - * @param packet - the type of packet to check. - * @return The ID of the given packet. - * @throws IllegalArgumentException If this is not a valid packet. - */ - public static int getPacketID(Class packet) { - if (packet == null) - throw new IllegalArgumentException("Packet type class cannot be NULL."); - if (!MinecraftReflection.getPacketClass().isAssignableFrom(packet)) - throw new IllegalArgumentException("Type must be a packet."); - - // The registry contains both the overridden and original packets - return getPacketToID().get(packet); - } - - /** - * Find the first superclass that is not a CBLib proxy object. - * @param clazz - the class whose hierachy we're going to search through. - * @param remove - whether or not to skip enhanced (proxy) classes. - * @return If remove is TRUE, the first superclass that is not a proxy. - */ - private static Class removeEnhancer(Class clazz, boolean remove) { - if (remove) { - // Get the underlying vanilla class - while (Factory.class.isAssignableFrom(clazz) && !clazz.equals(Object.class)) { - clazz = clazz.getSuperclass(); - } - } - - return clazz; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.packet; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.sf.cglib.proxy.Factory; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.TroveWrapper; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; + +/** + * Static packet registry in Minecraft. + * + * @author Kristian + */ +@SuppressWarnings("rawtypes") +public class PacketRegistry { + public static final ReportType REPORT_CANNOT_CORRECT_TROVE_MAP = new ReportType("Unable to correct no entry value."); + + public static final ReportType REPORT_INSUFFICIENT_SERVER_PACKETS = new ReportType("Too few server packets detected: %s"); + public static final ReportType REPORT_INSUFFICIENT_CLIENT_PACKETS = new ReportType("Too few client packets detected: %s"); + + private static final int MIN_SERVER_PACKETS = 5; + private static final int MIN_CLIENT_PACKETS = 5; + + // Fuzzy reflection + private static FuzzyReflection packetRegistry; + + // The packet class to packet ID translator + private static Map packetToID; + + // Whether or not certain packets are sent by the client or the server + private static ImmutableSet serverPackets; + private static ImmutableSet clientPackets; + + // The underlying sets + private static Set serverPacketsRef; + private static Set clientPacketsRef; + + // New proxy values + private static Map overwrittenPackets = new HashMap(); + + // Vanilla packets + private static Map previousValues = new HashMap(); + + @SuppressWarnings({ "unchecked" }) + public static Map getPacketToID() { + // Initialize it, if we haven't already + if (packetToID == null) { + try { + Field packetsField = getPacketRegistry().getFieldByType("packetsField", Map.class); + packetToID = (Map) FieldUtils.readStaticField(packetsField, true); + } catch (IllegalArgumentException e) { + // Spigot 1.2.5 MCPC workaround + try { + packetToID = getSpigotWrapper(); + } catch (Exception e2) { + // Very bad indeed + throw new IllegalArgumentException(e.getMessage() + "; Spigot workaround failed.", e2); + } + + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e); + } + } + + return packetToID; + } + + private static Map getSpigotWrapper() throws IllegalAccessException { + // If it talks like a duck, etc. + // Perhaps it would be nice to have a proper duck typing library as well + FuzzyClassContract mapLike = FuzzyClassContract.newBuilder(). + method(FuzzyMethodContract.newBuilder(). + nameExact("size").returnTypeExact(int.class)). + method(FuzzyMethodContract.newBuilder(). + nameExact("put").parameterCount(2)). + method(FuzzyMethodContract.newBuilder(). + nameExact("get").parameterCount(1)). + build(); + + Field packetsField = getPacketRegistry().getField( + FuzzyFieldContract.newBuilder().typeMatches(mapLike).build()); + Object troveMap = FieldUtils.readStaticField(packetsField, true); + + // Check for stupid no_entry_values + try { + Field field = FieldUtils.getField(troveMap.getClass(), "no_entry_value", true); + Integer value = (Integer) FieldUtils.readField(field, troveMap, true); + + if (value >= 0 && value < 256) { + // Someone forgot to set the no entry value. Let's help them. + FieldUtils.writeField(field, troveMap, -1); + } + } catch (IllegalArgumentException e) { + // Whatever + ProtocolLibrary.getErrorReporter().reportWarning(PacketRegistry.class, + Report.newBuilder(REPORT_CANNOT_CORRECT_TROVE_MAP).error(e)); + } + + // We'll assume this a Trove map + return TroveWrapper.getDecoratedMap(troveMap); + } + + /** + * Retrieve the cached fuzzy reflection instance allowing access to the packet registry. + * @return Reflected packet registry. + */ + private static FuzzyReflection getPacketRegistry() { + if (packetRegistry == null) + packetRegistry = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true); + return packetRegistry; + } + + /** + * Retrieve the injected proxy classes handlig each packet ID. + * @return Injected classes. + */ + public static Map getOverwrittenPackets() { + return overwrittenPackets; + } + + /** + * Retrieve the vanilla classes handling each packet ID. + * @return Vanilla classes. + */ + public static Map getPreviousPackets() { + return previousValues; + } + + /** + * Retrieve every known and supported server packet. + * @return An immutable set of every known server packet. + * @throws FieldAccessException If we're unable to retrieve the server packet data from Minecraft. + */ + public static Set getServerPackets() throws FieldAccessException { + initializeSets(); + + // Sanity check. This is impossible! + if (serverPackets != null && serverPackets.size() < MIN_SERVER_PACKETS) + throw new FieldAccessException("Server packet list is empty. Seems to be unsupported"); + return serverPackets; + } + + /** + * Retrieve every known and supported client packet. + * @return An immutable set of every known client packet. + * @throws FieldAccessException If we're unable to retrieve the client packet data from Minecraft. + */ + public static Set getClientPackets() throws FieldAccessException { + initializeSets(); + + // As above + if (clientPackets != null && clientPackets.size() < MIN_CLIENT_PACKETS) + throw new FieldAccessException("Client packet list is empty. Seems to be unsupported"); + return clientPackets; + } + + @SuppressWarnings("unchecked") + private static void initializeSets() throws FieldAccessException { + if (serverPacketsRef == null || clientPacketsRef == null) { + List sets = getPacketRegistry().getFieldListByType(Set.class); + + try { + if (sets.size() > 1) { + serverPacketsRef = (Set) FieldUtils.readStaticField(sets.get(0), true); + clientPacketsRef = (Set) FieldUtils.readStaticField(sets.get(1), true); + + // Impossible + if (serverPacketsRef == null || clientPacketsRef == null) + throw new FieldAccessException("Packet sets are in an illegal state."); + + // NEVER allow callers to modify the underlying sets + serverPackets = ImmutableSet.copyOf(serverPacketsRef); + clientPackets = ImmutableSet.copyOf(clientPacketsRef); + + // Check sizes + if (serverPackets.size() < MIN_SERVER_PACKETS) + ProtocolLibrary.getErrorReporter().reportWarning( + PacketRegistry.class, Report.newBuilder(REPORT_INSUFFICIENT_SERVER_PACKETS).messageParam(serverPackets.size()) + ); + if (clientPackets.size() < MIN_CLIENT_PACKETS) + ProtocolLibrary.getErrorReporter().reportWarning( + PacketRegistry.class, Report.newBuilder(REPORT_INSUFFICIENT_CLIENT_PACKETS).messageParam(clientPackets.size()) + ); + + } else { + throw new FieldAccessException("Cannot retrieve packet client/server sets."); + } + + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access field.", e); + } + + } else { + // Copy over again if it has changed + if (serverPacketsRef != null && serverPacketsRef.size() != serverPackets.size()) + serverPackets = ImmutableSet.copyOf(serverPacketsRef); + if (clientPacketsRef != null && clientPacketsRef.size() != clientPackets.size()) + clientPackets = ImmutableSet.copyOf(clientPacketsRef); + } + } + + /** + * Retrieves the correct packet class from a given packet ID. + * @param packetID - the packet ID. + * @return The associated class. + */ + public static Class getPacketClassFromID(int packetID) { + return getPacketClassFromID(packetID, false); + } + + /** + * Retrieves the correct packet class from a given packet ID. + * @param packetID - the packet ID. + * @param forceVanilla - whether or not to look for vanilla classes, not injected classes. + * @return The associated class. + */ + public static Class getPacketClassFromID(int packetID, boolean forceVanilla) { + + Map lookup = forceVanilla ? previousValues : overwrittenPackets; + + // Optimized lookup + if (lookup.containsKey(packetID)) { + return removeEnhancer(lookup.get(packetID), forceVanilla); + } + + // Will most likely not be used + for (Map.Entry entry : getPacketToID().entrySet()) { + if (Objects.equal(entry.getValue(), packetID)) { + // Attempt to get the vanilla class here too + if (!forceVanilla || MinecraftReflection.isMinecraftClass(entry.getKey())) + return removeEnhancer(entry.getKey(), forceVanilla); + } + } + + throw new IllegalArgumentException("The packet ID " + packetID + " is not registered."); + } + + /** + * Retrieve the packet ID of a given packet. + * @param packet - the type of packet to check. + * @return The ID of the given packet. + * @throws IllegalArgumentException If this is not a valid packet. + */ + public static int getPacketID(Class packet) { + if (packet == null) + throw new IllegalArgumentException("Packet type class cannot be NULL."); + if (!MinecraftReflection.getPacketClass().isAssignableFrom(packet)) + throw new IllegalArgumentException("Type must be a packet."); + + // The registry contains both the overridden and original packets + return getPacketToID().get(packet); + } + + /** + * Find the first superclass that is not a CBLib proxy object. + * @param clazz - the class whose hierachy we're going to search through. + * @param remove - whether or not to skip enhanced (proxy) classes. + * @return If remove is TRUE, the first superclass that is not a proxy. + */ + private static Class removeEnhancer(Class clazz, boolean remove) { + if (remove) { + // Get the underlying vanilla class + while (Factory.class.isAssignableFrom(clazz) && !clazz.equals(Object.class)) { + clazz = clazz.getSuperclass(); + } + } + + return clazz; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java index 67ff3da2..d88d8282 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java @@ -1,134 +1,140 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.packet; - -import java.io.DataInputStream; -import java.lang.reflect.Method; -import java.util.Map; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketEvent; -import com.google.common.collect.MapMaker; - -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - -class ReadPacketModifier implements MethodInterceptor { - // A cancel marker - private static final Object CANCEL_MARKER = new Object(); - - // Common for all packets of the same type - private ProxyPacketInjector packetInjector; - private int packetID; - - // Report errors - private ErrorReporter reporter; - - // If this is a read packet data method - private boolean isReadPacketDataMethod; - - // Whether or not a packet has been cancelled - private static Map override = new MapMaker().weakKeys().makeMap(); - - public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter, boolean isReadPacketDataMethod) { - this.packetID = packetID; - this.packetInjector = packetInjector; - this.reporter = reporter; - this.isReadPacketDataMethod = isReadPacketDataMethod; - } - - /** - * Remove any packet overrides. - * @param packet - the packet to rever - */ - public static void removeOverride(Object packet) { - override.remove(packet); - } - - /** - * Retrieve the packet that overrides the methods of the given packet. - * @param packet - the given packet. - * @return Overriden object. - */ - public static Object getOverride(Object packet) { - return override.get(packet); - } - - /** - * Determine if the given packet has been cancelled before. - * @param packet - the packet to check. - * @return TRUE if it has been cancelled, FALSE otherwise. - */ - public static boolean hasCancelled(Object packet) { - return getOverride(packet) == CANCEL_MARKER; - } - - @Override - public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - // Atomic retrieval - Object overridenObject = override.get(thisObj); - Object returnValue = null; - - if (overridenObject != null) { - // This packet has been cancelled - if (overridenObject == CANCEL_MARKER) { - // So, cancel all void methods - if (method.getReturnType().equals(Void.TYPE)) - return null; - else // Revert to normal for everything else - overridenObject = thisObj; - } - - returnValue = proxy.invokeSuper(overridenObject, args); - } else { - returnValue = proxy.invokeSuper(thisObj, args); - } - - // Is this a readPacketData method? - if (isReadPacketDataMethod) { - try { - // We need this in order to get the correct player - DataInputStream input = (DataInputStream) args[0]; - - // Let the people know - PacketContainer container = new PacketContainer(packetID, thisObj); - PacketEvent event = packetInjector.packetRecieved(container, input); - - // Handle override - if (event != null) { - Object result = event.getPacket().getHandle(); - - if (event.isCancelled()) { - override.put(thisObj, CANCEL_MARKER); - } else if (!objectEquals(thisObj, result)) { - override.put(thisObj, result); - } - } - } catch (Throwable e) { - // Minecraft cannot handle this error - reporter.reportDetailed(this, "Cannot handle client packet.", e, args[0]); - } - } - return returnValue; - } - - private boolean objectEquals(Object a, Object b) { - return System.identityHashCode(a) != System.identityHashCode(b); - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.packet; + +import java.io.DataInputStream; +import java.lang.reflect.Method; +import java.util.Map; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.google.common.collect.MapMaker; + +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +class ReadPacketModifier implements MethodInterceptor { + public static final ReportType REPORT_CANNOT_HANDLE_CLIENT_PACKET = new ReportType("Cannot handle client packet."); + + // A cancel marker + private static final Object CANCEL_MARKER = new Object(); + + // Common for all packets of the same type + private ProxyPacketInjector packetInjector; + private int packetID; + + // Report errors + private ErrorReporter reporter; + + // If this is a read packet data method + private boolean isReadPacketDataMethod; + + // Whether or not a packet has been cancelled + private static Map override = new MapMaker().weakKeys().makeMap(); + + public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter, boolean isReadPacketDataMethod) { + this.packetID = packetID; + this.packetInjector = packetInjector; + this.reporter = reporter; + this.isReadPacketDataMethod = isReadPacketDataMethod; + } + + /** + * Remove any packet overrides. + * @param packet - the packet to rever + */ + public static void removeOverride(Object packet) { + override.remove(packet); + } + + /** + * Retrieve the packet that overrides the methods of the given packet. + * @param packet - the given packet. + * @return Overriden object. + */ + public static Object getOverride(Object packet) { + return override.get(packet); + } + + /** + * Determine if the given packet has been cancelled before. + * @param packet - the packet to check. + * @return TRUE if it has been cancelled, FALSE otherwise. + */ + public static boolean hasCancelled(Object packet) { + return getOverride(packet) == CANCEL_MARKER; + } + + @Override + public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + // Atomic retrieval + Object overridenObject = override.get(thisObj); + Object returnValue = null; + + if (overridenObject != null) { + // This packet has been cancelled + if (overridenObject == CANCEL_MARKER) { + // So, cancel all void methods + if (method.getReturnType().equals(Void.TYPE)) + return null; + else // Revert to normal for everything else + overridenObject = thisObj; + } + + returnValue = proxy.invokeSuper(overridenObject, args); + } else { + returnValue = proxy.invokeSuper(thisObj, args); + } + + // Is this a readPacketData method? + if (isReadPacketDataMethod) { + try { + // We need this in order to get the correct player + DataInputStream input = (DataInputStream) args[0]; + + // Let the people know + PacketContainer container = new PacketContainer(packetID, thisObj); + PacketEvent event = packetInjector.packetRecieved(container, input); + + // Handle override + if (event != null) { + Object result = event.getPacket().getHandle(); + + if (event.isCancelled()) { + override.put(thisObj, CANCEL_MARKER); + } else if (!objectEquals(thisObj, result)) { + override.put(thisObj, result); + } + } + } catch (Throwable e) { + // Minecraft cannot handle this error + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_HANDLE_CLIENT_PACKET).callerParam(args[0]).error(e) + ); + } + } + return returnValue; + } + + private boolean objectEquals(Object a, Object b) { + return System.identityHashCode(a) != System.identityHashCode(b); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java index 57bf8e37..d8534215 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java @@ -1,175 +1,178 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Set; - -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.injector.ListenerInvoker; -import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket; - -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - -/** - * The array list that notifies when packets are sent by the server. - * - * @author Kristian - */ -class InjectedArrayList extends ArrayList { - - /** - * Silly Eclipse. - */ - private static final long serialVersionUID = -1173865905404280990L; - - private transient PlayerInjector injector; - private transient Set ignoredPackets; - private transient ClassLoader classLoader; - - private transient InvertedIntegerCallback callback; - - public InjectedArrayList(ClassLoader classLoader, PlayerInjector injector, Set ignoredPackets) { - this.classLoader = classLoader; - this.injector = injector; - this.ignoredPackets = ignoredPackets; - this.callback = new InvertedIntegerCallback(); - } - - @Override - public boolean add(Object packet) { - - Object result = null; - - // Check for fake packets and ignored packets - if (packet instanceof FakePacket) { - return true; - } else if (ignoredPackets.contains(packet)) { - // Don't send it to the filters - result = ignoredPackets.remove(packet); - } else { - result = injector.handlePacketSending(packet); - } - - // A NULL packet indicate cancelling - try { - if (result != null) { - super.add(result); - } else { - // We'll use the FakePacket marker instead of preventing the filters - injector.sendServerPacket(createNegativePacket(packet), true); - } - - // Collection.add contract - return true; - - } catch (InvocationTargetException e) { - ErrorReporter reporter = ProtocolLibrary.getErrorReporter(); - - // Prefer to report this to the user, instead of risking sending it to Minecraft - if (reporter != null) { - reporter.reportDetailed(this, "Reverting cancelled packet failed.", e, packet); - } else { - System.out.println("[ProtocolLib] Reverting cancelled packet failed."); - e.printStackTrace(); - } - - // Failure - return false; - } - } - - /** - * Used by a hack that reverses the effect of a cancelled packet. Returns a packet - * whereby every int method's return value is inverted (a => -a). - * - * @param source - packet to invert. - * @return The inverted packet. - */ - Object createNegativePacket(Object source) { - ListenerInvoker invoker = injector.getInvoker(); - - int packetID = invoker.getPacketID(source); - Class type = invoker.getPacketClassFromID(packetID, true); - - System.out.println(type.getName()); - - // We want to subtract the byte amount that were added to the running - // total of outstanding packets. Otherwise, cancelling too many packets - // might cause a "disconnect.overflow" error. - // - // We do that by constructing a special packet of the same type that returns - // a negative integer for all zero-parameter integer methods. This includes the - // size() method, which is used by the queue method to count the number of - // bytes to add. - // - // Essentially, we have: - // - // public class NegativePacket extends [a packet] { - // @Override - // public int size() { - // return -super.size(); - // } - // ect. - // } - Enhancer ex = new Enhancer(); - ex.setSuperclass(type); - ex.setInterfaces(new Class[] { FakePacket.class } ); - ex.setUseCache(true); - ex.setClassLoader(classLoader); - ex.setCallbackType(InvertedIntegerCallback.class); - - Class proxyClass = ex.createClass(); - Enhancer.registerCallbacks(proxyClass, new Callback[] { callback }); - - try { - // Temporarily associate the fake packet class - invoker.registerPacketClass(proxyClass, packetID); - return proxyClass.newInstance(); - - } catch (Exception e) { - // Don't pollute the throws tree - throw new RuntimeException("Cannot create fake class.", e); - } finally { - // Remove this association - invoker.unregisterPacketClass(proxyClass); - } - } - - /** - * Inverts the integer result of every integer method. - * @author Kristian - */ - private class InvertedIntegerCallback implements MethodInterceptor { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - if (method.getReturnType().equals(int.class) && args.length == 0) { - Integer result = (Integer) proxy.invokeSuper(obj, args); - return -result; - } else { - return proxy.invokeSuper(obj, args); - } - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Set; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +/** + * The array list that notifies when packets are sent by the server. + * + * @author Kristian + */ +class InjectedArrayList extends ArrayList { + public static final ReportType REPORT_CANNOT_REVERT_CANCELLED_PACKET = new ReportType("Reverting cancelled packet failed."); + + /** + * Silly Eclipse. + */ + private static final long serialVersionUID = -1173865905404280990L; + + private transient PlayerInjector injector; + private transient Set ignoredPackets; + private transient ClassLoader classLoader; + + private transient InvertedIntegerCallback callback; + + public InjectedArrayList(ClassLoader classLoader, PlayerInjector injector, Set ignoredPackets) { + this.classLoader = classLoader; + this.injector = injector; + this.ignoredPackets = ignoredPackets; + this.callback = new InvertedIntegerCallback(); + } + + @Override + public boolean add(Object packet) { + + Object result = null; + + // Check for fake packets and ignored packets + if (packet instanceof FakePacket) { + return true; + } else if (ignoredPackets.contains(packet)) { + // Don't send it to the filters + result = ignoredPackets.remove(packet); + } else { + result = injector.handlePacketSending(packet); + } + + // A NULL packet indicate cancelling + try { + if (result != null) { + super.add(result); + } else { + // We'll use the FakePacket marker instead of preventing the filters + injector.sendServerPacket(createNegativePacket(packet), true); + } + + // Collection.add contract + return true; + + } catch (InvocationTargetException e) { + ErrorReporter reporter = ProtocolLibrary.getErrorReporter(); + + // Prefer to report this to the user, instead of risking sending it to Minecraft + if (reporter != null) { + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet)); + } else { + System.out.println("[ProtocolLib] Reverting cancelled packet failed."); + e.printStackTrace(); + } + + // Failure + return false; + } + } + + /** + * Used by a hack that reverses the effect of a cancelled packet. Returns a packet + * whereby every int method's return value is inverted (a => -a). + * + * @param source - packet to invert. + * @return The inverted packet. + */ + Object createNegativePacket(Object source) { + ListenerInvoker invoker = injector.getInvoker(); + + int packetID = invoker.getPacketID(source); + Class type = invoker.getPacketClassFromID(packetID, true); + + System.out.println(type.getName()); + + // We want to subtract the byte amount that were added to the running + // total of outstanding packets. Otherwise, cancelling too many packets + // might cause a "disconnect.overflow" error. + // + // We do that by constructing a special packet of the same type that returns + // a negative integer for all zero-parameter integer methods. This includes the + // size() method, which is used by the queue method to count the number of + // bytes to add. + // + // Essentially, we have: + // + // public class NegativePacket extends [a packet] { + // @Override + // public int size() { + // return -super.size(); + // } + // ect. + // } + Enhancer ex = new Enhancer(); + ex.setSuperclass(type); + ex.setInterfaces(new Class[] { FakePacket.class } ); + ex.setUseCache(true); + ex.setClassLoader(classLoader); + ex.setCallbackType(InvertedIntegerCallback.class); + + Class proxyClass = ex.createClass(); + Enhancer.registerCallbacks(proxyClass, new Callback[] { callback }); + + try { + // Temporarily associate the fake packet class + invoker.registerPacketClass(proxyClass, packetID); + return proxyClass.newInstance(); + + } catch (Exception e) { + // Don't pollute the throws tree + throw new RuntimeException("Cannot create fake class.", e); + } finally { + // Remove this association + invoker.unregisterPacketClass(proxyClass); + } + } + + /** + * Inverts the integer result of every integer method. + * @author Kristian + */ + private class InvertedIntegerCallback implements MethodInterceptor { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + if (method.getReturnType().equals(int.class) && args.length == 0) { + Integer result = (Integer) proxy.invokeSuper(obj, args); + return -result; + } else { + return proxy.invokeSuper(obj, args); + } + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java index 4dd2fb9a..3f09fc5a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java @@ -1,318 +1,338 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -import net.sf.cglib.proxy.Factory; - -import org.bukkit.Server; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.reflect.ObjectWriter; -import com.comphenix.protocol.reflect.VolatileField; -import com.comphenix.protocol.utility.MinecraftReflection; - -/** - * Used to ensure that the 1.3 server is referencing the correct server handler. - * - * @author Kristian - */ -class InjectedServerConnection { - - private static Field listenerThreadField; - private static Field minecraftServerField; - private static Field listField; - private static Field dedicatedThreadField; - - private static Method serverConnectionMethod; - - private List listFields; - private List> replacedLists; - - // Used to inject net handlers - private NetLoginInjector netLoginInjector; - - // Inject server connections - private AbstractInputStreamLookup socketInjector; - - private Server server; - private ErrorReporter reporter; - private boolean hasAttempted; - private boolean hasSuccess; - - private Object minecraftServer = null; - - public InjectedServerConnection(ErrorReporter reporter, AbstractInputStreamLookup socketInjector, Server server, NetLoginInjector netLoginInjector) { - this.listFields = new ArrayList(); - this.replacedLists = new ArrayList>(); - this.reporter = reporter; - this.server = server; - this.socketInjector = socketInjector; - this.netLoginInjector = netLoginInjector; - } - - public void injectList() { - - // Only execute this method once - if (!hasAttempted) - hasAttempted = true; - else - return; - - if (minecraftServerField == null) - minecraftServerField = FuzzyReflection.fromObject(server, true). - getFieldByType("MinecraftServer", MinecraftReflection.getMinecraftServerClass()); - - try { - minecraftServer = FieldUtils.readField(minecraftServerField, server, true); - } catch (IllegalAccessException e1) { - reporter.reportWarning(this, "Cannot extract minecraft server from Bukkit."); - return; - } - - try { - if (serverConnectionMethod == null) - serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()). - getMethodByParameters("getServerConnection", - MinecraftReflection.getServerConnectionClass(), new Class[] {}); - // We're using Minecraft 1.3.1 - injectServerConnection(); - - } catch (IllegalArgumentException e) { - - // Minecraft 1.2.5 or lower - injectListenerThread(); - - } catch (Exception e) { - // Oh damn - inform the player - reporter.reportDetailed(this, "Cannot inject into server connection. Bad things will happen.", e); - } - } - - private void injectListenerThread() { - try { - if (listenerThreadField == null) - listenerThreadField = FuzzyReflection.fromObject(minecraftServer). - getFieldByType("networkListenThread", MinecraftReflection.getNetworkListenThreadClass()); - } catch (RuntimeException e) { - reporter.reportDetailed(this, "Cannot find listener thread in MinecraftServer.", e, minecraftServer); - return; - } - - Object listenerThread = null; - - // Attempt to get the thread - try { - listenerThread = listenerThreadField.get(minecraftServer); - } catch (Exception e) { - reporter.reportWarning(this, "Unable to read the listener thread.", e); - return; - } - - // Inject the server socket too - injectServerSocket(listenerThread); - - // Just inject every list field we can get - injectEveryListField(listenerThread, 1); - hasSuccess = true; - } - - private void injectServerConnection() { - - Object serverConnection = null; - - // Careful - we might fail - try { - serverConnection = serverConnectionMethod.invoke(minecraftServer); - } catch (Exception ex) { - reporter.reportDetailed(this, "Unable to retrieve server connection", ex, minecraftServer); - return; - } - - if (listField == null) - listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true). - getFieldByType("netServerHandlerList", List.class); - if (dedicatedThreadField == null) { - List matches = FuzzyReflection.fromObject(serverConnection, true). - getFieldListByType(Thread.class); - - // Verify the field count - if (matches.size() != 1) - reporter.reportWarning(this, "Unexpected number of threads in " + serverConnection.getClass().getName()); - else - dedicatedThreadField = matches.get(0); - } - - // Next, try to get the dedicated thread - try { - if (dedicatedThreadField != null) { - Object dedicatedThread = FieldUtils.readField(dedicatedThreadField, serverConnection, true); - - // Inject server socket and NetServerHandlers. - injectServerSocket(dedicatedThread); - injectEveryListField(dedicatedThread, 1); - } - } catch (IllegalAccessException e) { - reporter.reportWarning(this, "Unable to retrieve net handler thread.", e); - } - - injectIntoList(serverConnection, listField); - hasSuccess = true; - } - - private void injectServerSocket(Object container) { - socketInjector.inject(container); - } - - /** - * Automatically inject into every List-compatible public or private field of the given object. - * @param container - container object with the fields to inject. - * @param minimum - the minimum number of fields we expect exists. - */ - private void injectEveryListField(Object container, int minimum) { - // Ok, great. Get every list field - List lists = FuzzyReflection.fromObject(container, true).getFieldListByType(List.class); - - for (Field list : lists) { - injectIntoList(container, list); - } - - // Warn about unexpected errors - if (lists.size() < minimum) { - reporter.reportWarning(this, "Unable to inject " + minimum + " lists in " + container.getClass().getName()); - } - } - - @SuppressWarnings("unchecked") - private void injectIntoList(Object instance, Field field) { - VolatileField listFieldRef = new VolatileField(field, instance, true); - List list = (List) listFieldRef.getValue(); - - // Careful not to inject twice - if (list instanceof ReplacedArrayList) { - replacedLists.add((ReplacedArrayList) list); - } else { - ReplacedArrayList injectedList = createReplacement(list); - - replacedLists.add(injectedList); - listFieldRef.setValue(injectedList); - listFields.add(listFieldRef); - } - } - - // Hack to avoid the "moved to quickly" error - private ReplacedArrayList createReplacement(List list) { - return new ReplacedArrayList(list) { - /** - * Shut up Eclipse! - */ - private static final long serialVersionUID = 2070481080950500367L; - - // Object writer we'll use - private final ObjectWriter writer = new ObjectWriter(); - - @Override - protected void onReplacing(Object inserting, Object replacement) { - // Is this a normal Minecraft object? - if (!(inserting instanceof Factory)) { - // If so, copy the content of the old element to the new - try { - writer.copyTo(inserting, replacement, inserting.getClass()); - } catch (Throwable e) { - reporter.reportDetailed(InjectedServerConnection.this, "Cannot copy old " + inserting + - " to new.", e, inserting, replacement); - } - } - } - - @Override - protected void onInserting(Object inserting) { - // Ready for some login handler injection? - if (MinecraftReflection.isLoginHandler(inserting)) { - Object replaced = netLoginInjector.onNetLoginCreated(inserting); - - // Only replace if it has changed - if (inserting != replaced) - addMapping(inserting, replaced, true); - } - } - - @Override - protected void onRemoved(Object removing) { - // Clean up? - if (MinecraftReflection.isLoginHandler(removing)) { - netLoginInjector.cleanup(removing); - } - } - }; - } - - /** - * Replace the server handler instance kept by the "keep alive" object. - * @param oldHandler - old server handler. - * @param newHandler - new, proxied server handler. - */ - public void replaceServerHandler(Object oldHandler, Object newHandler) { - if (!hasAttempted) { - injectList(); - } - - if (hasSuccess) { - for (ReplacedArrayList replacedList : replacedLists) { - replacedList.addMapping(oldHandler, newHandler); - } - } - } - - /** - * Revert to the old vanilla server handler, if it has been replaced. - * @param oldHandler - old vanilla server handler. - */ - public void revertServerHandler(Object oldHandler) { - if (hasSuccess) { - for (ReplacedArrayList replacedList : replacedLists) { - replacedList.removeMapping(oldHandler); - } - } - } - - /** - * Undoes everything. - */ - public void cleanupAll() { - if (replacedLists.size() > 0) { - // Repair the underlying lists - for (ReplacedArrayList replacedList : replacedLists) { - replacedList.revertAll(); - } - for (VolatileField field : listFields) { - field.revertValue(); - } - - listFields.clear(); - replacedLists.clear(); - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import net.sf.cglib.proxy.Factory; + +import org.bukkit.Server; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.ObjectWriter; +import com.comphenix.protocol.reflect.VolatileField; +import com.comphenix.protocol.utility.MinecraftReflection; + +/** + * Used to ensure that the 1.3 server is referencing the correct server handler. + * + * @author Kristian + */ +class InjectedServerConnection { + // A number of things can go wrong ... + public static final ReportType REPORT_CANNOT_FIND_MINECRAFT_SERVER = new ReportType("Cannot extract minecraft server from Bukkit."); + public static final ReportType REPORT_CANNOT_INJECT_SERVER_CONNECTION = new ReportType("Cannot inject into server connection. Bad things will happen."); + + public static final ReportType REPORT_CANNOT_FIND_LISTENER_THREAD = new ReportType("Cannot find listener thread in MinecraftServer."); + public static final ReportType REPORT_CANNOT_READ_LISTENER_THREAD = new ReportType("Unable to read the listener thread."); + + public static final ReportType REPORT_CANNOT_FIND_SERVER_CONNECTION = new ReportType("Unable to retrieve server connection"); + public static final ReportType REPORT_UNEXPECTED_THREAD_COUNT = new ReportType("Unexpected number of threads in %s: %s"); + public static final ReportType REPORT_CANNOT_FIND_NET_HANDLER_THREAD = new ReportType("Unable to retrieve net handler thread."); + public static final ReportType REPORT_INSUFFICENT_THREAD_COUNT = new ReportType("Unable to inject %s lists in %s."); + + public static final ReportType REPORT_CANNOT_COPY_OLD_TO_NEW = new ReportType("Cannot copy old %s to new."); + + private static Field listenerThreadField; + private static Field minecraftServerField; + private static Field listField; + private static Field dedicatedThreadField; + + private static Method serverConnectionMethod; + + private List listFields; + private List> replacedLists; + + // Used to inject net handlers + private NetLoginInjector netLoginInjector; + + // Inject server connections + private AbstractInputStreamLookup socketInjector; + + private Server server; + private ErrorReporter reporter; + private boolean hasAttempted; + private boolean hasSuccess; + + private Object minecraftServer = null; + + public InjectedServerConnection(ErrorReporter reporter, AbstractInputStreamLookup socketInjector, Server server, NetLoginInjector netLoginInjector) { + this.listFields = new ArrayList(); + this.replacedLists = new ArrayList>(); + this.reporter = reporter; + this.server = server; + this.socketInjector = socketInjector; + this.netLoginInjector = netLoginInjector; + } + + public void injectList() { + // Only execute this method once + if (!hasAttempted) + hasAttempted = true; + else + return; + + if (minecraftServerField == null) + minecraftServerField = FuzzyReflection.fromObject(server, true). + getFieldByType("MinecraftServer", MinecraftReflection.getMinecraftServerClass()); + + try { + minecraftServer = FieldUtils.readField(minecraftServerField, server, true); + } catch (IllegalAccessException e1) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_FIND_MINECRAFT_SERVER)); + return; + } + + try { + if (serverConnectionMethod == null) + serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()). + getMethodByParameters("getServerConnection", + MinecraftReflection.getServerConnectionClass(), new Class[] {}); + // We're using Minecraft 1.3.1 + injectServerConnection(); + + } catch (IllegalArgumentException e) { + + // Minecraft 1.2.5 or lower + injectListenerThread(); + + } catch (Exception e) { + // Oh damn - inform the player + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_INJECT_SERVER_CONNECTION).error(e)); + } + } + + private void injectListenerThread() { + try { + if (listenerThreadField == null) + listenerThreadField = FuzzyReflection.fromObject(minecraftServer). + getFieldByType("networkListenThread", MinecraftReflection.getNetworkListenThreadClass()); + } catch (RuntimeException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_FIND_LISTENER_THREAD).callerParam(minecraftServer).error(e) + ); + return; + } + + Object listenerThread = null; + + // Attempt to get the thread + try { + listenerThread = listenerThreadField.get(minecraftServer); + } catch (Exception e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_READ_LISTENER_THREAD).error(e)); + return; + } + + // Inject the server socket too + injectServerSocket(listenerThread); + + // Just inject every list field we can get + injectEveryListField(listenerThread, 1); + hasSuccess = true; + } + + private void injectServerConnection() { + Object serverConnection = null; + + // Careful - we might fail + try { + serverConnection = serverConnectionMethod.invoke(minecraftServer); + } catch (Exception e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_FIND_SERVER_CONNECTION).callerParam(minecraftServer).error(e) + ); + return; + } + + if (listField == null) + listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true). + getFieldByType("netServerHandlerList", List.class); + if (dedicatedThreadField == null) { + List matches = FuzzyReflection.fromObject(serverConnection, true). + getFieldListByType(Thread.class); + + // Verify the field count + if (matches.size() != 1) + reporter.reportWarning(this, + Report.newBuilder(REPORT_UNEXPECTED_THREAD_COUNT).messageParam(serverConnection.getClass(), matches.size()) + ); + else + dedicatedThreadField = matches.get(0); + } + + // Next, try to get the dedicated thread + try { + if (dedicatedThreadField != null) { + Object dedicatedThread = FieldUtils.readField(dedicatedThreadField, serverConnection, true); + + // Inject server socket and NetServerHandlers. + injectServerSocket(dedicatedThread); + injectEveryListField(dedicatedThread, 1); + } + } catch (IllegalAccessException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_FIND_NET_HANDLER_THREAD).error(e)); + } + + injectIntoList(serverConnection, listField); + hasSuccess = true; + } + + private void injectServerSocket(Object container) { + socketInjector.inject(container); + } + + /** + * Automatically inject into every List-compatible public or private field of the given object. + * @param container - container object with the fields to inject. + * @param minimum - the minimum number of fields we expect exists. + */ + private void injectEveryListField(Object container, int minimum) { + // Ok, great. Get every list field + List lists = FuzzyReflection.fromObject(container, true).getFieldListByType(List.class); + + for (Field list : lists) { + injectIntoList(container, list); + } + + // Warn about unexpected errors + if (lists.size() < minimum) { + reporter.reportWarning(this, Report.newBuilder(REPORT_INSUFFICENT_THREAD_COUNT).messageParam(minimum, container.getClass())); + } + } + + @SuppressWarnings("unchecked") + private void injectIntoList(Object instance, Field field) { + VolatileField listFieldRef = new VolatileField(field, instance, true); + List list = (List) listFieldRef.getValue(); + + // Careful not to inject twice + if (list instanceof ReplacedArrayList) { + replacedLists.add((ReplacedArrayList) list); + } else { + ReplacedArrayList injectedList = createReplacement(list); + + replacedLists.add(injectedList); + listFieldRef.setValue(injectedList); + listFields.add(listFieldRef); + } + } + + // Hack to avoid the "moved to quickly" error + private ReplacedArrayList createReplacement(List list) { + return new ReplacedArrayList(list) { + /** + * Shut up Eclipse! + */ + private static final long serialVersionUID = 2070481080950500367L; + + // Object writer we'll use + private final ObjectWriter writer = new ObjectWriter(); + + @Override + protected void onReplacing(Object inserting, Object replacement) { + // Is this a normal Minecraft object? + if (!(inserting instanceof Factory)) { + // If so, copy the content of the old element to the new + try { + writer.copyTo(inserting, replacement, inserting.getClass()); + } catch (Throwable e) { + reporter.reportDetailed(InjectedServerConnection.this, + Report.newBuilder(REPORT_CANNOT_COPY_OLD_TO_NEW).messageParam(inserting).callerParam(inserting, replacement).error(e) + ); + } + } + } + + @Override + protected void onInserting(Object inserting) { + // Ready for some login handler injection? + if (MinecraftReflection.isLoginHandler(inserting)) { + Object replaced = netLoginInjector.onNetLoginCreated(inserting); + + // Only replace if it has changed + if (inserting != replaced) + addMapping(inserting, replaced, true); + } + } + + @Override + protected void onRemoved(Object removing) { + // Clean up? + if (MinecraftReflection.isLoginHandler(removing)) { + netLoginInjector.cleanup(removing); + } + } + }; + } + + /** + * Replace the server handler instance kept by the "keep alive" object. + * @param oldHandler - old server handler. + * @param newHandler - new, proxied server handler. + */ + public void replaceServerHandler(Object oldHandler, Object newHandler) { + if (!hasAttempted) { + injectList(); + } + + if (hasSuccess) { + for (ReplacedArrayList replacedList : replacedLists) { + replacedList.addMapping(oldHandler, newHandler); + } + } + } + + /** + * Revert to the old vanilla server handler, if it has been replaced. + * @param oldHandler - old vanilla server handler. + */ + public void revertServerHandler(Object oldHandler) { + if (hasSuccess) { + for (ReplacedArrayList replacedList : replacedLists) { + replacedList.removeMapping(oldHandler); + } + } + } + + /** + * Undoes everything. + */ + public void cleanupAll() { + if (replacedLists.size() > 0) { + // Repair the underlying lists + for (ReplacedArrayList replacedList : replacedLists) { + replacedList.revertAll(); + } + for (VolatileField field : listFields) { + field.revertValue(); + } + + listFields.clear(); + replacedLists.clear(); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java index 0fc27113..129a4bc1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java @@ -1,141 +1,154 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.util.concurrent.ConcurrentMap; - -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy; -import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.google.common.collect.Maps; - -/** - * Injects every NetLoginHandler created by the server. - * - * @author Kristian - */ -class NetLoginInjector { - private ConcurrentMap injectedLogins = Maps.newConcurrentMap(); - - // Handles every hook - private ProxyPlayerInjectionHandler injectionHandler; - - // Create temporary players - private TemporaryPlayerFactory playerFactory = new TemporaryPlayerFactory(); - - // The current error reporter - private ErrorReporter reporter; - private Server server; - - public NetLoginInjector(ErrorReporter reporter, Server server, ProxyPlayerInjectionHandler injectionHandler) { - this.reporter = reporter; - this.server = server; - this.injectionHandler = injectionHandler; - } - - /** - * Invoked when a NetLoginHandler has been created. - * @param inserting - the new NetLoginHandler. - * @return An injected NetLoginHandler, or the original object. - */ - public Object onNetLoginCreated(Object inserting) { - try { - // Make sure we actually need to inject during this phase - if (!injectionHandler.isInjectionNecessary(GamePhase.LOGIN)) - return inserting; - - Player temporary = playerFactory.createTemporaryPlayer(server); - // Note that we bail out if there's an existing player injector - PlayerInjector injector = injectionHandler.injectPlayer( - temporary, inserting, ConflictStrategy.BAIL_OUT, GamePhase.LOGIN); - - if (injector != null) { - // Update injector as well - TemporaryPlayerFactory.setInjectorInPlayer(temporary, injector); - injector.updateOnLogin = true; - - // Save the login - injectedLogins.putIfAbsent(inserting, injector); - } - - // NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler - return inserting; - - } catch (Throwable e) { - // Minecraft can't handle this, so we'll deal with it here - reporter.reportDetailed(this, "Unable to hook " + - MinecraftReflection.getNetLoginHandlerName() + ".", e, inserting, injectionHandler); - return inserting; - } - } - - /** - * Invoked when a NetLoginHandler should be reverted. - * @param inserting - the original NetLoginHandler. - * @return An injected NetLoginHandler, or the original object. - */ - public synchronized void cleanup(Object removing) { - PlayerInjector injected = injectedLogins.get(removing); - - if (injected != null) { - try { - PlayerInjector newInjector = null; - Player player = injected.getPlayer(); - - // Clean up list - injectedLogins.remove(removing); - - // No need to clean up twice - if (injected.isClean()) - return; - - // Hack to clean up other references - newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager()); - injectionHandler.uninjectPlayer(player); - - // Update NetworkManager - if (newInjector != null) { - if (injected instanceof NetworkObjectInjector) { - newInjector.setNetworkManager(injected.getNetworkManager(), true); - } - } - - } catch (Throwable e) { - // Don't leak this to Minecraft - reporter.reportDetailed(this, "Cannot cleanup " + - MinecraftReflection.getNetLoginHandlerName() + ".", e, removing); - } - } - } - - /** - * Remove all injected hooks. - */ - public void cleanupAll() { - for (PlayerInjector injector : injectedLogins.values()) { - injector.cleanupAll(); - } - - injectedLogins.clear(); - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.util.concurrent.ConcurrentMap; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.Maps; + +/** + * Injects every NetLoginHandler created by the server. + * + * @author Kristian + */ +class NetLoginInjector { + public static final ReportType REPORT_CANNOT_HOOK_LOGIN_HANDLER = new ReportType("Unable to hook %s."); + public static final ReportType REPORT_CANNOT_CLEANUP_LOGIN_HANDLER = new ReportType("Cannot cleanup %s."); + + private ConcurrentMap injectedLogins = Maps.newConcurrentMap(); + + // Handles every hook + private ProxyPlayerInjectionHandler injectionHandler; + + // Create temporary players + private TemporaryPlayerFactory playerFactory = new TemporaryPlayerFactory(); + + // The current error reporter + private ErrorReporter reporter; + private Server server; + + public NetLoginInjector(ErrorReporter reporter, Server server, ProxyPlayerInjectionHandler injectionHandler) { + this.reporter = reporter; + this.server = server; + this.injectionHandler = injectionHandler; + } + + /** + * Invoked when a NetLoginHandler has been created. + * @param inserting - the new NetLoginHandler. + * @return An injected NetLoginHandler, or the original object. + */ + public Object onNetLoginCreated(Object inserting) { + try { + // Make sure we actually need to inject during this phase + if (!injectionHandler.isInjectionNecessary(GamePhase.LOGIN)) + return inserting; + + Player temporary = playerFactory.createTemporaryPlayer(server); + // Note that we bail out if there's an existing player injector + PlayerInjector injector = injectionHandler.injectPlayer( + temporary, inserting, ConflictStrategy.BAIL_OUT, GamePhase.LOGIN); + + if (injector != null) { + // Update injector as well + TemporaryPlayerFactory.setInjectorInPlayer(temporary, injector); + injector.updateOnLogin = true; + + // Save the login + injectedLogins.putIfAbsent(inserting, injector); + } + + // NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler + return inserting; + + } catch (Throwable e) { + // Minecraft can't handle this, so we'll deal with it here + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_HOOK_LOGIN_HANDLER). + messageParam(MinecraftReflection.getNetLoginHandlerName()). + callerParam(inserting, injectionHandler). + error(e) + ); + return inserting; + } + } + + /** + * Invoked when a NetLoginHandler should be reverted. + * @param inserting - the original NetLoginHandler. + * @return An injected NetLoginHandler, or the original object. + */ + public synchronized void cleanup(Object removing) { + PlayerInjector injected = injectedLogins.get(removing); + + if (injected != null) { + try { + PlayerInjector newInjector = null; + Player player = injected.getPlayer(); + + // Clean up list + injectedLogins.remove(removing); + + // No need to clean up twice + if (injected.isClean()) + return; + + // Hack to clean up other references + newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager()); + injectionHandler.uninjectPlayer(player); + + // Update NetworkManager + if (newInjector != null) { + if (injected instanceof NetworkObjectInjector) { + newInjector.setNetworkManager(injected.getNetworkManager(), true); + } + } + + } catch (Throwable e) { + // Don't leak this to Minecraft + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_CLEANUP_LOGIN_HANDLER). + messageParam(MinecraftReflection.getNetLoginHandlerName()). + callerParam(removing). + error(e) + ); + } + } + } + + /** + * Remove all injected hooks. + */ + public void cleanupAll() { + for (PlayerInjector injector : injectedLogins.values()) { + injector.cleanupAll(); + } + + injectedLogins.clear(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java index 610d1a8b..c9a0b6bb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java @@ -1,345 +1,352 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; - -import net.sf.cglib.proxy.*; - -import org.bukkit.entity.Player; - -import com.comphenix.protocol.concurrency.IntegerSet; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.PacketListener; -import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.injector.ListenerInvoker; -import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.reflect.ObjectWriter; -import com.comphenix.protocol.reflect.VolatileField; -import com.comphenix.protocol.reflect.instances.DefaultInstances; -import com.comphenix.protocol.reflect.instances.ExistingGenerator; -import com.comphenix.protocol.utility.MinecraftMethods; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.comphenix.protocol.utility.MinecraftVersion; - -/** - * Represents a player hook into the NetServerHandler class. - * - * @author Kristian - */ -class NetworkServerInjector extends PlayerInjector { - private volatile static CallbackFilter callbackFilter; - private volatile static boolean foundSendPacket; - - private volatile static Field disconnectField; - private InjectedServerConnection serverInjection; - - // Determine if we're listening - private IntegerSet sendingFilters; - - // Used to create proxy objects - private ClassLoader classLoader; - - // Whether or not the player has disconnected - private boolean hasDisconnected; - - // Used to copy fields - private final ObjectWriter writer = new ObjectWriter(); - - public NetworkServerInjector( - ClassLoader classLoader, ErrorReporter reporter, Player player, - ListenerInvoker invoker, IntegerSet sendingFilters, - InjectedServerConnection serverInjection) throws IllegalAccessException { - - super(reporter, player, invoker); - this.classLoader = classLoader; - this.sendingFilters = sendingFilters; - this.serverInjection = serverInjection; - } - - @Override - protected boolean hasListener(int packetID) { - return sendingFilters.contains(packetID); - } - - @Override - public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException { - Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue(); - - if (serverDelegate != null) { - try { - // Note that invocation target exception is a wrapper for a checked exception - MinecraftMethods.getSendPacketMethod().invoke(serverDelegate, packet); - - } catch (IllegalArgumentException e) { - throw e; - } catch (InvocationTargetException e) { - throw e; - } catch (IllegalAccessException e) { - throw new IllegalStateException("Unable to access send packet method.", e); - } - } else { - throw new IllegalStateException("Unable to load server handler. Cannot send packet."); - } - } - - @Override - public void injectManager() { - - if (serverHandlerRef == null) - throw new IllegalStateException("Cannot find server handler."); - // Don't inject twice - if (serverHandlerRef.getValue() instanceof Factory) - return; - - if (!tryInjectManager()) { - Class serverHandlerClass = MinecraftReflection.getNetServerHandlerClass(); - - // Try to override the proxied object - if (proxyServerField != null) { - serverHandlerRef = new VolatileField(proxyServerField, serverHandler, true); - serverHandler = serverHandlerRef.getValue(); - - if (serverHandler == null) - throw new RuntimeException("Cannot hook player: Inner proxy object is NULL."); - else - serverHandlerClass = serverHandler.getClass(); - - // Try again - if (tryInjectManager()) { - // It worked - probably - return; - } - } - - throw new RuntimeException( - "Cannot hook player: Unable to find a valid constructor for the " - + serverHandlerClass.getName() + " object."); - } - } - - private boolean tryInjectManager() { - Class serverClass = serverHandler.getClass(); - - Enhancer ex = new Enhancer(); - Callback sendPacketCallback = new MethodInterceptor() { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - Object packet = args[0]; - - if (packet != null) { - packet = handlePacketSending(packet); - - // A NULL packet indicate cancelling - if (packet != null) - args[0] = packet; - else - return null; - } - - // Call the method directly - return proxy.invokeSuper(obj, args); - }; - }; - Callback noOpCallback = NoOp.INSTANCE; - - // Share callback filter - that way, we avoid generating a new class for - // every logged in player. - if (callbackFilter == null) { - final Method sendPacket = MinecraftMethods.getSendPacketMethod(); - - callbackFilter = new CallbackFilter() { - @Override - public int accept(Method method) { - if (isCallableEqual(sendPacket, method)) { - foundSendPacket = true; - return 0; - } else { - return 1; - } - } - }; - } - - ex.setClassLoader(classLoader); - ex.setSuperclass(serverClass); - ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback }); - ex.setCallbackFilter(callbackFilter); - - // Find the Minecraft NetServerHandler superclass - Class minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass()); - ExistingGenerator generator = ExistingGenerator.fromObjectFields(serverHandler, minecraftSuperClass); - DefaultInstances serverInstances = null; - - // Maybe the proxy instance can help? - Object proxyInstance = getProxyServerHandler(); - - // Use the existing server proxy when we create one - if (proxyInstance != null && proxyInstance != serverHandler) { - serverInstances = DefaultInstances.fromArray(generator, - ExistingGenerator.fromObjectArray(new Object[] { proxyInstance })); - } else { - serverInstances = DefaultInstances.fromArray(generator); - } - - serverInstances.setNonNull(true); - serverInstances.setMaximumRecursion(1); - - Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass); - - // Inject it now - if (proxyObject != null) { - // Did we override a sendPacket method? - if (!foundSendPacket) { - throw new IllegalArgumentException("Unable to find a sendPacket method in " + serverClass); - } - - serverInjection.replaceServerHandler(serverHandler, proxyObject); - serverHandlerRef.setValue(proxyObject); - return true; - } else { - return false; - } - } - - /** - * Determine if the two methods are equal in terms of call semantics. - *

- * Two methods are equal if they have the same name, parameter types and return type. - * @param first - first method. - * @param second - second method. - * @return TRUE if they are, FALSE otherwise. - */ - private boolean isCallableEqual(Method first, Method second) { - return first.getName().equals(second.getName()) && - first.getReturnType().equals(second.getReturnType()) && - Arrays.equals(first.getParameterTypes(), second.getParameterTypes()); - } - - private Object getProxyServerHandler() { - if (proxyServerField != null && !proxyServerField.equals(serverHandlerRef.getField())) { - try { - return FieldUtils.readField(proxyServerField, serverHandler, true); - } catch (Throwable e) { - // Oh well - } - } - - return null; - } - - private Class getFirstMinecraftSuperClass(Class clazz) { - if (MinecraftReflection.isMinecraftClass(clazz)) - return clazz; - else if (clazz.equals(Object.class)) - return clazz; - else - return getFirstMinecraftSuperClass(clazz.getSuperclass()); - } - - @Override - protected void cleanHook() { - if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) { - writer.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass()); - serverHandlerRef.revertValue(); - - try { - if (getNetHandler() != null) { - // Restore packet listener - try { - FieldUtils.writeField(netHandlerField, networkManager, serverHandlerRef.getOldValue(), true); - } catch (IllegalAccessException e) { - // Oh well - e.printStackTrace(); - } - } - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - // Prevent the PlayerQuitEvent from being sent twice - if (hasDisconnected) { - setDisconnect(serverHandlerRef.getValue(), true); - } - } - - serverInjection.revertServerHandler(serverHandler); - } - - @Override - public void handleDisconnect() { - hasDisconnected = true; - } - - /** - * Set the disconnected field in a NetServerHandler. - * @param handler - the NetServerHandler. - * @param value - the new value. - */ - private void setDisconnect(Object handler, boolean value) { - // Set it - try { - // Load the field - if (disconnectField == null) { - disconnectField = FuzzyReflection.fromObject(handler).getFieldByName("disconnected.*"); - } - FieldUtils.writeField(disconnectField, handler, value); - - } catch (IllegalArgumentException e) { - // Assume it's the first ... - if (disconnectField == null) { - disconnectField = FuzzyReflection.fromObject(handler).getFieldByType("disconnected", boolean.class); - reporter.reportWarning(this, "Unable to find 'disconnected' field. Assuming " + disconnectField); - - // Try again - if (disconnectField != null) { - setDisconnect(handler, value); - return; - } - } - - // This is really bad - reporter.reportDetailed(this, "Cannot find disconnected field. Is ProtocolLib up to date?", e); - - } catch (IllegalAccessException e) { - reporter.reportWarning(this, "Unable to update disconnected field. Player quit event may be sent twice."); - } - } - - @Override - public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { - // We support everything - return null; - } - - @Override - public boolean canInject(GamePhase phase) { - // Doesn't work when logging in - return phase == GamePhase.PLAYING; - } - - @Override - public PlayerInjectHooks getHookType() { - return PlayerInjectHooks.NETWORK_SERVER_OBJECT; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +import net.sf.cglib.proxy.*; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.ObjectWriter; +import com.comphenix.protocol.reflect.VolatileField; +import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.comphenix.protocol.reflect.instances.ExistingGenerator; +import com.comphenix.protocol.utility.MinecraftMethods; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; + +/** + * Represents a player hook into the NetServerHandler class. + * + * @author Kristian + */ +class NetworkServerInjector extends PlayerInjector { + // Disconnected field + public static final ReportType REPORT_ASSUMING_DISCONNECT_FIELD = new ReportType("Unable to find 'disconnected' field. Assuming %s."); + public static final ReportType REPORT_DISCONNECT_FIELD_MISSING = new ReportType("Cannot find disconnected field. Is ProtocolLib up to date?"); + public static final ReportType REPORT_DISCONNECT_FIELD_FAILURE = new ReportType("Unable to update disconnected field. Player quit event may be sent twice."); + + private volatile static CallbackFilter callbackFilter; + private volatile static boolean foundSendPacket; + + private volatile static Field disconnectField; + private InjectedServerConnection serverInjection; + + // Determine if we're listening + private IntegerSet sendingFilters; + + // Used to create proxy objects + private ClassLoader classLoader; + + // Whether or not the player has disconnected + private boolean hasDisconnected; + + // Used to copy fields + private final ObjectWriter writer = new ObjectWriter(); + + public NetworkServerInjector( + ClassLoader classLoader, ErrorReporter reporter, Player player, + ListenerInvoker invoker, IntegerSet sendingFilters, + InjectedServerConnection serverInjection) throws IllegalAccessException { + + super(reporter, player, invoker); + this.classLoader = classLoader; + this.sendingFilters = sendingFilters; + this.serverInjection = serverInjection; + } + + @Override + protected boolean hasListener(int packetID) { + return sendingFilters.contains(packetID); + } + + @Override + public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException { + Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue(); + + if (serverDelegate != null) { + try { + // Note that invocation target exception is a wrapper for a checked exception + MinecraftMethods.getSendPacketMethod().invoke(serverDelegate, packet); + + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw e; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unable to access send packet method.", e); + } + } else { + throw new IllegalStateException("Unable to load server handler. Cannot send packet."); + } + } + + @Override + public void injectManager() { + + if (serverHandlerRef == null) + throw new IllegalStateException("Cannot find server handler."); + // Don't inject twice + if (serverHandlerRef.getValue() instanceof Factory) + return; + + if (!tryInjectManager()) { + Class serverHandlerClass = MinecraftReflection.getNetServerHandlerClass(); + + // Try to override the proxied object + if (proxyServerField != null) { + serverHandlerRef = new VolatileField(proxyServerField, serverHandler, true); + serverHandler = serverHandlerRef.getValue(); + + if (serverHandler == null) + throw new RuntimeException("Cannot hook player: Inner proxy object is NULL."); + else + serverHandlerClass = serverHandler.getClass(); + + // Try again + if (tryInjectManager()) { + // It worked - probably + return; + } + } + + throw new RuntimeException( + "Cannot hook player: Unable to find a valid constructor for the " + + serverHandlerClass.getName() + " object."); + } + } + + private boolean tryInjectManager() { + Class serverClass = serverHandler.getClass(); + + Enhancer ex = new Enhancer(); + Callback sendPacketCallback = new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Object packet = args[0]; + + if (packet != null) { + packet = handlePacketSending(packet); + + // A NULL packet indicate cancelling + if (packet != null) + args[0] = packet; + else + return null; + } + + // Call the method directly + return proxy.invokeSuper(obj, args); + }; + }; + Callback noOpCallback = NoOp.INSTANCE; + + // Share callback filter - that way, we avoid generating a new class for + // every logged in player. + if (callbackFilter == null) { + final Method sendPacket = MinecraftMethods.getSendPacketMethod(); + + callbackFilter = new CallbackFilter() { + @Override + public int accept(Method method) { + if (isCallableEqual(sendPacket, method)) { + foundSendPacket = true; + return 0; + } else { + return 1; + } + } + }; + } + + ex.setClassLoader(classLoader); + ex.setSuperclass(serverClass); + ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback }); + ex.setCallbackFilter(callbackFilter); + + // Find the Minecraft NetServerHandler superclass + Class minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass()); + ExistingGenerator generator = ExistingGenerator.fromObjectFields(serverHandler, minecraftSuperClass); + DefaultInstances serverInstances = null; + + // Maybe the proxy instance can help? + Object proxyInstance = getProxyServerHandler(); + + // Use the existing server proxy when we create one + if (proxyInstance != null && proxyInstance != serverHandler) { + serverInstances = DefaultInstances.fromArray(generator, + ExistingGenerator.fromObjectArray(new Object[] { proxyInstance })); + } else { + serverInstances = DefaultInstances.fromArray(generator); + } + + serverInstances.setNonNull(true); + serverInstances.setMaximumRecursion(1); + + Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass); + + // Inject it now + if (proxyObject != null) { + // Did we override a sendPacket method? + if (!foundSendPacket) { + throw new IllegalArgumentException("Unable to find a sendPacket method in " + serverClass); + } + + serverInjection.replaceServerHandler(serverHandler, proxyObject); + serverHandlerRef.setValue(proxyObject); + return true; + } else { + return false; + } + } + + /** + * Determine if the two methods are equal in terms of call semantics. + *

+ * Two methods are equal if they have the same name, parameter types and return type. + * @param first - first method. + * @param second - second method. + * @return TRUE if they are, FALSE otherwise. + */ + private boolean isCallableEqual(Method first, Method second) { + return first.getName().equals(second.getName()) && + first.getReturnType().equals(second.getReturnType()) && + Arrays.equals(first.getParameterTypes(), second.getParameterTypes()); + } + + private Object getProxyServerHandler() { + if (proxyServerField != null && !proxyServerField.equals(serverHandlerRef.getField())) { + try { + return FieldUtils.readField(proxyServerField, serverHandler, true); + } catch (Throwable e) { + // Oh well + } + } + + return null; + } + + private Class getFirstMinecraftSuperClass(Class clazz) { + if (MinecraftReflection.isMinecraftClass(clazz)) + return clazz; + else if (clazz.equals(Object.class)) + return clazz; + else + return getFirstMinecraftSuperClass(clazz.getSuperclass()); + } + + @Override + protected void cleanHook() { + if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) { + writer.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass()); + serverHandlerRef.revertValue(); + + try { + if (getNetHandler() != null) { + // Restore packet listener + try { + FieldUtils.writeField(netHandlerField, networkManager, serverHandlerRef.getOldValue(), true); + } catch (IllegalAccessException e) { + // Oh well + e.printStackTrace(); + } + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + // Prevent the PlayerQuitEvent from being sent twice + if (hasDisconnected) { + setDisconnect(serverHandlerRef.getValue(), true); + } + } + + serverInjection.revertServerHandler(serverHandler); + } + + @Override + public void handleDisconnect() { + hasDisconnected = true; + } + + /** + * Set the disconnected field in a NetServerHandler. + * @param handler - the NetServerHandler. + * @param value - the new value. + */ + private void setDisconnect(Object handler, boolean value) { + // Set it + try { + // Load the field + if (disconnectField == null) { + disconnectField = FuzzyReflection.fromObject(handler).getFieldByName("disconnected.*"); + } + FieldUtils.writeField(disconnectField, handler, value); + + } catch (IllegalArgumentException e) { + // Assume it's the first ... + if (disconnectField == null) { + disconnectField = FuzzyReflection.fromObject(handler).getFieldByType("disconnected", boolean.class); + reporter.reportWarning(this, Report.newBuilder(REPORT_ASSUMING_DISCONNECT_FIELD).messageParam(disconnectField)); + + // Try again + if (disconnectField != null) { + setDisconnect(handler, value); + return; + } + } + + // This is really bad + reporter.reportDetailed(this, Report.newBuilder(REPORT_DISCONNECT_FIELD_MISSING).error(e)); + + } catch (IllegalAccessException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_DISCONNECT_FIELD_FAILURE).error(e)); + } + } + + @Override + public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { + // We support everything + return null; + } + + @Override + public boolean canInject(GamePhase phase) { + // Doesn't work when logging in + return phase == GamePhase.PLAYING; + } + + @Override + public PlayerInjectHooks getHookType() { + return PlayerInjectHooks.NETWORK_SERVER_OBJECT; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java index caff1e2c..f1f177da 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java @@ -1,649 +1,664 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.io.DataInputStream; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.Socket; -import java.net.SocketAddress; -import net.sf.cglib.proxy.Factory; - -import org.bukkit.entity.Player; - -import com.comphenix.protocol.Packets; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.events.PacketListener; -import com.comphenix.protocol.injector.BukkitUnwrapper; -import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.injector.ListenerInvoker; -import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; -import com.comphenix.protocol.injector.server.SocketInjector; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.reflect.StructureModifier; -import com.comphenix.protocol.reflect.VolatileField; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.comphenix.protocol.utility.MinecraftVersion; - -abstract class PlayerInjector implements SocketInjector { - - // Net login handler stuff - private static Field netLoginNetworkField; - - // Different disconnect methods - private static Method loginDisconnect; - private static Method serverDisconnect; - - // Cache previously retrieved fields - protected static Field serverHandlerField; - protected static Field proxyServerField; - - protected static Field networkManagerField; - protected static Field netHandlerField; - protected static Field socketField; - protected static Field socketAddressField; - - private static Field inputField; - private static Field entityPlayerField; - - // Whether or not we're using a proxy type - private static boolean hasProxyType; - - // To add our injected array lists - protected static StructureModifier networkModifier; - - // And methods - protected static Method queueMethod; - protected static Method processMethod; - - protected Player player; - protected boolean hasInitialized; - - // Reference to the player's network manager - protected VolatileField networkManagerRef; - protected VolatileField serverHandlerRef; - protected Object networkManager; - - // Current net handler - protected Object loginHandler; - protected Object serverHandler; - protected Object netHandler; - - // Current socket and address - protected Socket socket; - protected SocketAddress socketAddress; - - // The packet manager and filters - protected ListenerInvoker invoker; - - // Previous data input - protected DataInputStream cachedInput; - - // Handle errors - protected ErrorReporter reporter; - - // Whether or not the injector has been cleaned - private boolean clean; - - // Whether or not to update the current player on the first Packet1Login - boolean updateOnLogin; - Player updatedPlayer; - - public PlayerInjector(ErrorReporter reporter, Player player, ListenerInvoker invoker) throws IllegalAccessException { - this.reporter = reporter; - this.player = player; - this.invoker = invoker; - } - - /** - * Retrieve the notch (NMS) entity player object. - * @param player - the player to retrieve. - * @return Notch player object. - */ - protected Object getEntityPlayer(Player player) { - BukkitUnwrapper unwrapper = new BukkitUnwrapper(); - return unwrapper.unwrapItem(player); - } - - /** - * Initialize all fields for this player injector, if it hasn't already. - * @throws IllegalAccessException An error has occured. - */ - public void initialize(Object injectionSource) throws IllegalAccessException { - if (injectionSource == null) - throw new IllegalArgumentException("injectionSource cannot be NULL"); - - //Dispatch to the correct injection method - if (injectionSource instanceof Player) - initializePlayer((Player) injectionSource); - else if (MinecraftReflection.isLoginHandler(injectionSource)) - initializeLogin(injectionSource); - else - throw new IllegalArgumentException("Cannot initialize a player hook using a " + injectionSource.getClass().getName()); - } - - /** - * Initialize the player injector using an actual player instance. - * @param player - the player to hook. - */ - public void initializePlayer(Player player) { - Object notchEntity = getEntityPlayer((Player) player); - - // Save the player too - this.player = player; - - if (!hasInitialized) { - // Do this first, in case we encounter an exception - hasInitialized = true; - - // Retrieve the server handler - if (serverHandlerField == null) { - serverHandlerField = FuzzyReflection.fromObject(notchEntity).getFieldByType( - "NetServerHandler", MinecraftReflection.getNetServerHandlerClass()); - proxyServerField = getProxyField(notchEntity, serverHandlerField); - } - - // Yo dawg - serverHandlerRef = new VolatileField(serverHandlerField, notchEntity); - serverHandler = serverHandlerRef.getValue(); - - // Next, get the network manager - if (networkManagerField == null) - networkManagerField = FuzzyReflection.fromObject(serverHandler).getFieldByType( - "networkManager", MinecraftReflection.getNetworkManagerClass()); - initializeNetworkManager(networkManagerField, serverHandler); - } - } - - /** - * Initialize the player injector from a NetLoginHandler. - * @param netLoginHandler - the net login handler to inject. - */ - public void initializeLogin(Object netLoginHandler) { - if (!hasInitialized) { - // Just in case - if (!MinecraftReflection.isLoginHandler(netLoginHandler)) - throw new IllegalArgumentException("netLoginHandler (" + netLoginHandler + ") is not a " + - MinecraftReflection.getNetLoginHandlerName()); - - hasInitialized = true; - loginHandler = netLoginHandler; - - if (netLoginNetworkField == null) - netLoginNetworkField = FuzzyReflection.fromObject(netLoginHandler). - getFieldByType("networkManager", MinecraftReflection.getNetworkManagerClass()); - initializeNetworkManager(netLoginNetworkField, netLoginHandler); - } - } - - private void initializeNetworkManager(Field reference, Object container) { - networkManagerRef = new VolatileField(reference, container); - networkManager = networkManagerRef.getValue(); - - // No, don't do it - if (networkManager instanceof Factory) { - return; - } - - // Create the network manager modifier from the actual object type - if (networkManager != null && networkModifier == null) - networkModifier = new StructureModifier(networkManager.getClass(), null, false); - - // And the queue method - if (queueMethod == null) - queueMethod = FuzzyReflection.fromClass(reference.getType()). - getMethodByParameters("queue", MinecraftReflection.getPacketClass()); - } - - /** - * Retrieve whether or not the server handler is a proxy object. - * @return TRUE if it is, FALSE otherwise. - */ - protected boolean hasProxyServerHandler() { - return hasProxyType; - } - - /** - * Retrieve the current network manager. - * @return Current network manager. - */ - public Object getNetworkManager() { - return networkManagerRef.getValue(); - } - - /** - * Set the current network manager. - * @param value - new network manager. - * @param force - whether or not to save this value. - */ - public void setNetworkManager(Object value, boolean force) { - networkManagerRef.setValue(value); - - if (force) - networkManagerRef.saveValue(); - initializeNetworkManager(networkManagerField, serverHandler); - } - - /** - * Retrieve the associated socket of this player. - * @return The associated socket. - * @throws IllegalAccessException If we're unable to read the socket field. - */ - @Override - public Socket getSocket() throws IllegalAccessException { - try { - if (socketField == null) - socketField = FuzzyReflection.fromObject(networkManager, true). - getFieldListByType(Socket.class).get(0); - if (socket == null) - socket = (Socket) FieldUtils.readField(socketField, networkManager, true); - return socket; - - } catch (IndexOutOfBoundsException e) { - throw new IllegalAccessException("Unable to read the socket field."); - } - } - - /** - * Retrieve the associated remote address of a player. - * @return The associated remote address.. - * @throws IllegalAccessException If we're unable to read the socket address field. - */ - @Override - public SocketAddress getAddress() throws IllegalAccessException { - try { - if (socketAddressField == null) - socketAddressField = FuzzyReflection.fromObject(networkManager, true). - getFieldListByType(SocketAddress.class).get(0); - if (socketAddress == null) - socketAddress = (SocketAddress) FieldUtils.readField(socketAddressField, networkManager, true); - return socketAddress; - - } catch (IndexOutOfBoundsException e) { - throw new IllegalAccessException("Unable to read the socket address field."); - } - } - - /** - * Attempt to disconnect the current client. - * @param message - the message to display. - * @throws InvocationTargetException If disconnection failed. - */ - @Override - public void disconnect(String message) throws InvocationTargetException { - // Get a non-null handler - boolean usingNetServer = serverHandler != null; - - Object handler = usingNetServer ? serverHandler : loginHandler; - Method disconnect = usingNetServer ? serverDisconnect : loginDisconnect; - - // Execute disconnect on it - if (handler != null) { - if (disconnect == null) { - try { - disconnect = FuzzyReflection.fromObject(handler).getMethodByName("disconnect.*"); - } catch (IllegalArgumentException e) { - // Just assume it's the first String method - disconnect = FuzzyReflection.fromObject(handler).getMethodByParameters("disconnect", String.class); - reporter.reportWarning(this, "Cannot find disconnect method by name. Assuming " + disconnect); - } - - // Save the method for later - if (usingNetServer) - serverDisconnect = disconnect; - else - loginDisconnect = disconnect; - } - - try { - disconnect.invoke(handler, message); - return; - } catch (IllegalArgumentException e) { - reporter.reportDetailed(this, "Invalid argument passed to disconnect method: " + message, e, handler); - } catch (IllegalAccessException e) { - reporter.reportWarning(this, "Unable to access disconnect method.", e); - } - } - - // Fuck it - try { - Socket socket = getSocket(); - - try { - socket.close(); - } catch (IOException e) { - reporter.reportDetailed(this, "Unable to close socket.", e, socket); - } - - } catch (IllegalAccessException e) { - reporter.reportWarning(this, "Insufficient permissions. Cannot close socket.", e); - } - } - - private Field getProxyField(Object notchEntity, Field serverField) { - - try { - Object handler = FieldUtils.readField(serverHandlerField, notchEntity, true); - - // Is this a Minecraft hook? - if (handler != null && !MinecraftReflection.isMinecraftObject(handler)) { - - // This is our proxy object - if (handler instanceof Factory) - return null; - - hasProxyType = true; - reporter.reportWarning(this, "Detected server handler proxy type by another plugin. Conflict may occur!"); - - // No? Is it a Proxy type? - try { - FuzzyReflection reflection = FuzzyReflection.fromObject(handler, true); - - // It might be - return reflection.getFieldByType("NetServerHandler", MinecraftReflection.getNetServerHandlerClass()); - - } catch (RuntimeException e) { - // Damn - } - } - - } catch (IllegalAccessException e) { - reporter.reportWarning(this, "Unable to load server handler from proxy type."); - } - - // Nope, just go with it - return null; - } - - /** - * Retrieves the current net handler for this player. - * @return Current net handler. - * @throws IllegalAccessException Unable to find or retrieve net handler. - */ - protected Object getNetHandler() throws IllegalAccessException { - - // What a mess - try { - if (netHandlerField == null) - netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). - getFieldByType("NetHandler", MinecraftReflection.getNetHandlerClass()); - } catch (RuntimeException e1) { - // Swallow it - } - - // Second attempt - if (netHandlerField == null) { - try { - // Well, that sucks. Try just Minecraft objects then. - netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). - getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); - - } catch (RuntimeException e2) { - throw new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); - } - } - - // Get the handler - if (netHandler == null) - netHandler = FieldUtils.readField(netHandlerField, networkManager, true); - return netHandler; - } - - /** - * Retrieve the stored entity player from a given NetHandler. - * @param netHandler - the nethandler to retrieve it from. - * @return The stored entity player. - * @throws IllegalAccessException If the reflection failed. - */ - private Object getEntityPlayer(Object netHandler) throws IllegalAccessException { - if (entityPlayerField == null) - entityPlayerField = FuzzyReflection.fromObject(netHandler).getFieldByType( - "EntityPlayer", MinecraftReflection.getEntityPlayerClass()); - return FieldUtils.readField(entityPlayerField, netHandler); - } - - /** - * Processes the given packet as if it was transmitted by the current player. - * @param packet - packet to process. - * @throws IllegalAccessException If the reflection machinery failed. - * @throws InvocationTargetException If the underlying method caused an error. - */ - public void processPacket(Object packet) throws IllegalAccessException, InvocationTargetException { - - Object netHandler = getNetHandler(); - - // Get the process method - if (processMethod == null) { - try { - processMethod = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()). - getMethodByParameters("processPacket", netHandlerField.getType()); - } catch (RuntimeException e) { - throw new IllegalArgumentException("Cannot locate process packet method: " + e.getMessage()); - } - } - - // We're ready - try { - processMethod.invoke(packet, netHandler); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Method " + processMethod.getName() + " is not compatible."); - } catch (InvocationTargetException e) { - throw e; - } - } - - /** - * Send a packet to the client. - * @param packet - server packet to send. - * @param filtered - whether or not the packet will be filtered by our listeners. - * @param InvocationTargetException If an error occured when sending the packet. - */ - @Override - public abstract void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException; - - /** - * Inject a hook to catch packets sent to the current player. - */ - public abstract void injectManager(); - - /** - * Remove all hooks and modifications. - */ - public final void cleanupAll() { - if (!clean) - cleanHook(); - clean = true; - } - - /** - * Clean up after the player has disconnected. - */ - public abstract void handleDisconnect(); - - /** - * Override to add custom cleanup behavior. - */ - protected abstract void cleanHook(); - - /** - * Determine whether or not this hook has already been cleaned. - * @return TRUE if it has, FALSE otherwise. - */ - public boolean isClean() { - return clean; - } - - /** - * Determine if this inject method can even be attempted. - * @return TRUE if can be attempted, though possibly with failure, FALSE otherwise. - */ - public abstract boolean canInject(GamePhase state); - - /** - * Retrieve the hook type this class represents. - * @return Hook type this class represents. - */ - public abstract PlayerInjectHooks getHookType(); - - /** - * Invoked before a new listener is registered. - *

- * The player injector should only return a non-null value if some or all of the packet IDs are unsupported. - * @param version - * - * @param version - the current Minecraft version, or NULL if unknown. - * @param listener - the listener that is about to be registered. - * @return A error message with the unsupported packet IDs, or NULL if this listener is valid. - */ - public abstract UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener); - - /** - * Allows a packet to be sent by the listeners. - * @param packet - packet to sent. - * @return The given packet, or the packet replaced by the listeners. - */ - public Object handlePacketSending(Object packet) { - try { - // Get the packet ID too - Integer id = invoker.getPacketID(packet); - Player currentPlayer = player; - - // Hack #1 - if (updateOnLogin) { - if (id == Packets.Server.LOGIN) { - try { - updatedPlayer = (Player) MinecraftReflection.getBukkitEntity(getEntityPlayer(getNetHandler())); - } catch (IllegalAccessException e) { - reporter.reportDetailed(this, "Cannot update player in PlayerEvent.", e, packet); - } - } - - // This will only occur in the NetLoginHandler injection - if (updatedPlayer != null) - currentPlayer = updatedPlayer; - } - - // Make sure we're listening - if (id != null && hasListener(id)) { - // A packet has been sent guys! - PacketContainer container = new PacketContainer(id, packet); - PacketEvent event = PacketEvent.fromServer(invoker, container, currentPlayer); - invoker.invokePacketSending(event); - - // Cancelling is pretty simple. Just ignore the packet. - if (event.isCancelled()) - return null; - - // Right, remember to replace the packet again - return event.getPacket().getHandle(); - } - - } catch (Throwable e) { - reporter.reportDetailed(this, "Cannot handle server packet.", e, packet); - } - - return packet; - } - - /** - * Determine if the given injector is listening for this packet ID. - * @param packetID - packet ID to check. - * @return TRUE if it is, FALSE oterhwise. - */ - protected abstract boolean hasListener(int packetID); - - /** - * Retrieve the current player's input stream. - * @param cache - whether or not to cache the result of this method. - * @return The player's input stream. - */ - public DataInputStream getInputStream(boolean cache) { - // And the data input stream that we'll use to identify a player - if (networkManager == null) - throw new IllegalStateException("Network manager is NULL."); - if (inputField == null) - inputField = FuzzyReflection.fromObject(networkManager, true). - getFieldByType("java\\.io\\.DataInputStream"); - - // Get the associated input stream - try { - if (cache && cachedInput != null) - return cachedInput; - - // Save to cache - cachedInput = (DataInputStream) FieldUtils.readField(inputField, networkManager, true); - return cachedInput; - - } catch (IllegalAccessException e) { - throw new RuntimeException("Unable to read input stream.", e); - } - } - - /** - * Retrieve the hooked player. - */ - @Override - public Player getPlayer() { - return player; - } - - /** - * Set the hooked player. - *

- * Should only be called during the creation of the injector. - * @param player - the new hooked player. - */ - public void setPlayer(Player player) { - this.player = player; - } - - /** - * Object that can invoke the packet events. - * @return Packet event invoker. - */ - public ListenerInvoker getInvoker() { - return invoker; - } - - /** - * Retrieve the hooked player object OR the more up-to-date player instance. - * @return The hooked player, or a more up-to-date instance. - */ - @Override - public Player getUpdatedPlayer() { - if (updatedPlayer != null) - return updatedPlayer; - else - return player; - } - - @Override - public void transferState(SocketInjector delegate) { - // Do nothing - } - - @Override - public void setUpdatedPlayer(Player updatedPlayer) { - this.updatedPlayer = updatedPlayer; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.io.DataInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.SocketAddress; +import net.sf.cglib.proxy.Factory; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.BukkitUnwrapper; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.comphenix.protocol.injector.server.SocketInjector; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.VolatileField; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; + +public abstract class PlayerInjector implements SocketInjector { + // Disconnect method related reports + public static final ReportType REPORT_ASSUME_DISCONNECT_METHOD = new ReportType("Cannot find disconnect method by name. Assuming %s."); + public static final ReportType REPORT_INVALID_ARGUMENT_DISCONNECT = new ReportType("Invalid argument passed to disconnect method: %s"); + public static final ReportType REPORT_CANNOT_ACCESS_DISCONNECT = new ReportType("Unable to access disconnect method."); + + public static final ReportType REPORT_CANNOT_CLOSE_SOCKET = new ReportType("Unable to close socket."); + public static final ReportType REPORT_ACCESS_DENIED_CLOSE_SOCKET = new ReportType("Insufficient permissions. Cannot close socket."); + + public static final ReportType REPORT_DETECTED_CUSTOM_SERVER_HANDLER = + new ReportType("Detected server handler proxy type by another plugin. Conflict may occur!"); + public static final ReportType REPORT_CANNOT_PROXY_SERVER_HANDLER = new ReportType("Unable to load server handler from proxy type."); + + public static final ReportType REPORT_CANNOT_UPDATE_PLAYER = new ReportType("Cannot update player in PlayerEvent."); + public static final ReportType REPORT_CANNOT_HANDLE_PACKET = new ReportType("Cannot handle server packet."); + + // Net login handler stuff + private static Field netLoginNetworkField; + + // Different disconnect methods + private static Method loginDisconnect; + private static Method serverDisconnect; + + // Cache previously retrieved fields + protected static Field serverHandlerField; + protected static Field proxyServerField; + + protected static Field networkManagerField; + protected static Field netHandlerField; + protected static Field socketField; + protected static Field socketAddressField; + + private static Field inputField; + private static Field entityPlayerField; + + // Whether or not we're using a proxy type + private static boolean hasProxyType; + + // To add our injected array lists + protected static StructureModifier networkModifier; + + // And methods + protected static Method queueMethod; + protected static Method processMethod; + + protected Player player; + protected boolean hasInitialized; + + // Reference to the player's network manager + protected VolatileField networkManagerRef; + protected VolatileField serverHandlerRef; + protected Object networkManager; + + // Current net handler + protected Object loginHandler; + protected Object serverHandler; + protected Object netHandler; + + // Current socket and address + protected Socket socket; + protected SocketAddress socketAddress; + + // The packet manager and filters + protected ListenerInvoker invoker; + + // Previous data input + protected DataInputStream cachedInput; + + // Handle errors + protected ErrorReporter reporter; + + // Whether or not the injector has been cleaned + private boolean clean; + + // Whether or not to update the current player on the first Packet1Login + boolean updateOnLogin; + Player updatedPlayer; + + public PlayerInjector(ErrorReporter reporter, Player player, ListenerInvoker invoker) throws IllegalAccessException { + this.reporter = reporter; + this.player = player; + this.invoker = invoker; + } + + /** + * Retrieve the notch (NMS) entity player object. + * @param player - the player to retrieve. + * @return Notch player object. + */ + protected Object getEntityPlayer(Player player) { + BukkitUnwrapper unwrapper = new BukkitUnwrapper(); + return unwrapper.unwrapItem(player); + } + + /** + * Initialize all fields for this player injector, if it hasn't already. + * @throws IllegalAccessException An error has occured. + */ + public void initialize(Object injectionSource) throws IllegalAccessException { + if (injectionSource == null) + throw new IllegalArgumentException("injectionSource cannot be NULL"); + + //Dispatch to the correct injection method + if (injectionSource instanceof Player) + initializePlayer((Player) injectionSource); + else if (MinecraftReflection.isLoginHandler(injectionSource)) + initializeLogin(injectionSource); + else + throw new IllegalArgumentException("Cannot initialize a player hook using a " + injectionSource.getClass().getName()); + } + + /** + * Initialize the player injector using an actual player instance. + * @param player - the player to hook. + */ + public void initializePlayer(Player player) { + Object notchEntity = getEntityPlayer((Player) player); + + // Save the player too + this.player = player; + + if (!hasInitialized) { + // Do this first, in case we encounter an exception + hasInitialized = true; + + // Retrieve the server handler + if (serverHandlerField == null) { + serverHandlerField = FuzzyReflection.fromObject(notchEntity).getFieldByType( + "NetServerHandler", MinecraftReflection.getNetServerHandlerClass()); + proxyServerField = getProxyField(notchEntity, serverHandlerField); + } + + // Yo dawg + serverHandlerRef = new VolatileField(serverHandlerField, notchEntity); + serverHandler = serverHandlerRef.getValue(); + + // Next, get the network manager + if (networkManagerField == null) + networkManagerField = FuzzyReflection.fromObject(serverHandler).getFieldByType( + "networkManager", MinecraftReflection.getNetworkManagerClass()); + initializeNetworkManager(networkManagerField, serverHandler); + } + } + + /** + * Initialize the player injector from a NetLoginHandler. + * @param netLoginHandler - the net login handler to inject. + */ + public void initializeLogin(Object netLoginHandler) { + if (!hasInitialized) { + // Just in case + if (!MinecraftReflection.isLoginHandler(netLoginHandler)) + throw new IllegalArgumentException("netLoginHandler (" + netLoginHandler + ") is not a " + + MinecraftReflection.getNetLoginHandlerName()); + + hasInitialized = true; + loginHandler = netLoginHandler; + + if (netLoginNetworkField == null) + netLoginNetworkField = FuzzyReflection.fromObject(netLoginHandler). + getFieldByType("networkManager", MinecraftReflection.getNetworkManagerClass()); + initializeNetworkManager(netLoginNetworkField, netLoginHandler); + } + } + + private void initializeNetworkManager(Field reference, Object container) { + networkManagerRef = new VolatileField(reference, container); + networkManager = networkManagerRef.getValue(); + + // No, don't do it + if (networkManager instanceof Factory) { + return; + } + + // Create the network manager modifier from the actual object type + if (networkManager != null && networkModifier == null) + networkModifier = new StructureModifier(networkManager.getClass(), null, false); + + // And the queue method + if (queueMethod == null) + queueMethod = FuzzyReflection.fromClass(reference.getType()). + getMethodByParameters("queue", MinecraftReflection.getPacketClass()); + } + + /** + * Retrieve whether or not the server handler is a proxy object. + * @return TRUE if it is, FALSE otherwise. + */ + protected boolean hasProxyServerHandler() { + return hasProxyType; + } + + /** + * Retrieve the current network manager. + * @return Current network manager. + */ + public Object getNetworkManager() { + return networkManagerRef.getValue(); + } + + /** + * Set the current network manager. + * @param value - new network manager. + * @param force - whether or not to save this value. + */ + public void setNetworkManager(Object value, boolean force) { + networkManagerRef.setValue(value); + + if (force) + networkManagerRef.saveValue(); + initializeNetworkManager(networkManagerField, serverHandler); + } + + /** + * Retrieve the associated socket of this player. + * @return The associated socket. + * @throws IllegalAccessException If we're unable to read the socket field. + */ + @Override + public Socket getSocket() throws IllegalAccessException { + try { + if (socketField == null) + socketField = FuzzyReflection.fromObject(networkManager, true). + getFieldListByType(Socket.class).get(0); + if (socket == null) + socket = (Socket) FieldUtils.readField(socketField, networkManager, true); + return socket; + + } catch (IndexOutOfBoundsException e) { + throw new IllegalAccessException("Unable to read the socket field."); + } + } + + /** + * Retrieve the associated remote address of a player. + * @return The associated remote address.. + * @throws IllegalAccessException If we're unable to read the socket address field. + */ + @Override + public SocketAddress getAddress() throws IllegalAccessException { + try { + if (socketAddressField == null) + socketAddressField = FuzzyReflection.fromObject(networkManager, true). + getFieldListByType(SocketAddress.class).get(0); + if (socketAddress == null) + socketAddress = (SocketAddress) FieldUtils.readField(socketAddressField, networkManager, true); + return socketAddress; + + } catch (IndexOutOfBoundsException e) { + throw new IllegalAccessException("Unable to read the socket address field."); + } + } + + /** + * Attempt to disconnect the current client. + * @param message - the message to display. + * @throws InvocationTargetException If disconnection failed. + */ + @Override + public void disconnect(String message) throws InvocationTargetException { + // Get a non-null handler + boolean usingNetServer = serverHandler != null; + + Object handler = usingNetServer ? serverHandler : loginHandler; + Method disconnect = usingNetServer ? serverDisconnect : loginDisconnect; + + // Execute disconnect on it + if (handler != null) { + if (disconnect == null) { + try { + disconnect = FuzzyReflection.fromObject(handler).getMethodByName("disconnect.*"); + } catch (IllegalArgumentException e) { + // Just assume it's the first String method + disconnect = FuzzyReflection.fromObject(handler).getMethodByParameters("disconnect", String.class); + reporter.reportWarning(this, Report.newBuilder(REPORT_ASSUME_DISCONNECT_METHOD).messageParam(disconnect)); + } + + // Save the method for later + if (usingNetServer) + serverDisconnect = disconnect; + else + loginDisconnect = disconnect; + } + + try { + disconnect.invoke(handler, message); + return; + } catch (IllegalArgumentException e) { + reporter.reportDetailed(this, Report.newBuilder(REPORT_INVALID_ARGUMENT_DISCONNECT).error(e).messageParam(message).callerParam(handler)); + } catch (IllegalAccessException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_ACCESS_DISCONNECT).error(e)); + } + } + + // Fuck it + try { + Socket socket = getSocket(); + + try { + socket.close(); + } catch (IOException e) { + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CLOSE_SOCKET).error(e).callerParam(socket)); + } + + } catch (IllegalAccessException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_ACCESS_DENIED_CLOSE_SOCKET).error(e)); + } + } + + private Field getProxyField(Object notchEntity, Field serverField) { + try { + Object handler = FieldUtils.readField(serverHandlerField, notchEntity, true); + + // Is this a Minecraft hook? + if (handler != null && !MinecraftReflection.isMinecraftObject(handler)) { + + // This is our proxy object + if (handler instanceof Factory) + return null; + + hasProxyType = true; + reporter.reportWarning(this, Report.newBuilder(REPORT_DETECTED_CUSTOM_SERVER_HANDLER).callerParam(notchEntity, serverField)); + + // No? Is it a Proxy type? + try { + FuzzyReflection reflection = FuzzyReflection.fromObject(handler, true); + + // It might be + return reflection.getFieldByType("NetServerHandler", MinecraftReflection.getNetServerHandlerClass()); + + } catch (RuntimeException e) { + // Damn + } + } + + } catch (IllegalAccessException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PROXY_SERVER_HANDLER).error(e).callerParam(notchEntity, serverField)); + } + + // Nope, just go with it + return null; + } + + /** + * Retrieves the current net handler for this player. + * @return Current net handler. + * @throws IllegalAccessException Unable to find or retrieve net handler. + */ + protected Object getNetHandler() throws IllegalAccessException { + + // What a mess + try { + if (netHandlerField == null) + netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). + getFieldByType("NetHandler", MinecraftReflection.getNetHandlerClass()); + } catch (RuntimeException e1) { + // Swallow it + } + + // Second attempt + if (netHandlerField == null) { + try { + // Well, that sucks. Try just Minecraft objects then. + netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). + getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); + + } catch (RuntimeException e2) { + throw new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); + } + } + + // Get the handler + if (netHandler == null) + netHandler = FieldUtils.readField(netHandlerField, networkManager, true); + return netHandler; + } + + /** + * Retrieve the stored entity player from a given NetHandler. + * @param netHandler - the nethandler to retrieve it from. + * @return The stored entity player. + * @throws IllegalAccessException If the reflection failed. + */ + private Object getEntityPlayer(Object netHandler) throws IllegalAccessException { + if (entityPlayerField == null) + entityPlayerField = FuzzyReflection.fromObject(netHandler).getFieldByType( + "EntityPlayer", MinecraftReflection.getEntityPlayerClass()); + return FieldUtils.readField(entityPlayerField, netHandler); + } + + /** + * Processes the given packet as if it was transmitted by the current player. + * @param packet - packet to process. + * @throws IllegalAccessException If the reflection machinery failed. + * @throws InvocationTargetException If the underlying method caused an error. + */ + public void processPacket(Object packet) throws IllegalAccessException, InvocationTargetException { + + Object netHandler = getNetHandler(); + + // Get the process method + if (processMethod == null) { + try { + processMethod = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()). + getMethodByParameters("processPacket", netHandlerField.getType()); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Cannot locate process packet method: " + e.getMessage()); + } + } + + // We're ready + try { + processMethod.invoke(packet, netHandler); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Method " + processMethod.getName() + " is not compatible."); + } catch (InvocationTargetException e) { + throw e; + } + } + + /** + * Send a packet to the client. + * @param packet - server packet to send. + * @param filtered - whether or not the packet will be filtered by our listeners. + * @param InvocationTargetException If an error occured when sending the packet. + */ + @Override + public abstract void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException; + + /** + * Inject a hook to catch packets sent to the current player. + */ + public abstract void injectManager(); + + /** + * Remove all hooks and modifications. + */ + public final void cleanupAll() { + if (!clean) + cleanHook(); + clean = true; + } + + /** + * Clean up after the player has disconnected. + */ + public abstract void handleDisconnect(); + + /** + * Override to add custom cleanup behavior. + */ + protected abstract void cleanHook(); + + /** + * Determine whether or not this hook has already been cleaned. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean isClean() { + return clean; + } + + /** + * Determine if this inject method can even be attempted. + * @return TRUE if can be attempted, though possibly with failure, FALSE otherwise. + */ + public abstract boolean canInject(GamePhase state); + + /** + * Retrieve the hook type this class represents. + * @return Hook type this class represents. + */ + public abstract PlayerInjectHooks getHookType(); + + /** + * Invoked before a new listener is registered. + *

+ * The player injector should only return a non-null value if some or all of the packet IDs are unsupported. + * @param version + * + * @param version - the current Minecraft version, or NULL if unknown. + * @param listener - the listener that is about to be registered. + * @return A error message with the unsupported packet IDs, or NULL if this listener is valid. + */ + public abstract UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener); + + /** + * Allows a packet to be sent by the listeners. + * @param packet - packet to sent. + * @return The given packet, or the packet replaced by the listeners. + */ + public Object handlePacketSending(Object packet) { + try { + // Get the packet ID too + Integer id = invoker.getPacketID(packet); + Player currentPlayer = player; + + // Hack #1 + if (updateOnLogin) { + if (id == Packets.Server.LOGIN) { + try { + updatedPlayer = (Player) MinecraftReflection.getBukkitEntity(getEntityPlayer(getNetHandler())); + } catch (IllegalAccessException e) { + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLAYER).error(e).callerParam(packet)); + } + } + + // This will only occur in the NetLoginHandler injection + if (updatedPlayer != null) + currentPlayer = updatedPlayer; + } + + // Make sure we're listening + if (id != null && hasListener(id)) { + // A packet has been sent guys! + PacketContainer container = new PacketContainer(id, packet); + PacketEvent event = PacketEvent.fromServer(invoker, container, currentPlayer); + invoker.invokePacketSending(event); + + // Cancelling is pretty simple. Just ignore the packet. + if (event.isCancelled()) + return null; + + // Right, remember to replace the packet again + return event.getPacket().getHandle(); + } + + } catch (Throwable e) { + reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_HANDLE_PACKET).error(e).callerParam(packet)); + } + + return packet; + } + + /** + * Determine if the given injector is listening for this packet ID. + * @param packetID - packet ID to check. + * @return TRUE if it is, FALSE oterhwise. + */ + protected abstract boolean hasListener(int packetID); + + /** + * Retrieve the current player's input stream. + * @param cache - whether or not to cache the result of this method. + * @return The player's input stream. + */ + public DataInputStream getInputStream(boolean cache) { + // And the data input stream that we'll use to identify a player + if (networkManager == null) + throw new IllegalStateException("Network manager is NULL."); + if (inputField == null) + inputField = FuzzyReflection.fromObject(networkManager, true). + getFieldByType("java\\.io\\.DataInputStream"); + + // Get the associated input stream + try { + if (cache && cachedInput != null) + return cachedInput; + + // Save to cache + cachedInput = (DataInputStream) FieldUtils.readField(inputField, networkManager, true); + return cachedInput; + + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to read input stream.", e); + } + } + + /** + * Retrieve the hooked player. + */ + @Override + public Player getPlayer() { + return player; + } + + /** + * Set the hooked player. + *

+ * Should only be called during the creation of the injector. + * @param player - the new hooked player. + */ + public void setPlayer(Player player) { + this.player = player; + } + + /** + * Object that can invoke the packet events. + * @return Packet event invoker. + */ + public ListenerInvoker getInvoker() { + return invoker; + } + + /** + * Retrieve the hooked player object OR the more up-to-date player instance. + * @return The hooked player, or a more up-to-date instance. + */ + @Override + public Player getUpdatedPlayer() { + if (updatedPlayer != null) + return updatedPlayer; + else + return player; + } + + @Override + public void transferState(SocketInjector delegate) { + // Do nothing + } + + @Override + public void setUpdatedPlayer(Player updatedPlayer) { + this.updatedPlayer = updatedPlayer; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java index 2329d6b1..99f6c550 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java @@ -1,696 +1,710 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.io.DataInputStream; -import java.lang.reflect.InvocationTargetException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import net.sf.cglib.proxy.Factory; - -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.Packets; -import com.comphenix.protocol.concurrency.BlockingHashMap; -import com.comphenix.protocol.concurrency.IntegerSet; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketListener; -import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.injector.ListenerInvoker; -import com.comphenix.protocol.injector.PlayerLoggedOutException; -import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; -import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; -import com.comphenix.protocol.injector.server.BukkitSocketInjector; -import com.comphenix.protocol.injector.server.InputStreamLookupBuilder; -import com.comphenix.protocol.injector.server.SocketInjector; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.comphenix.protocol.utility.MinecraftVersion; - -import com.google.common.base.Predicate; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.Maps; - -/** - * Responsible for injecting into a player's sendPacket method. - * - * @author Kristian - */ -class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { - // Server connection injection - private InjectedServerConnection serverInjection; - - // Server socket injection - private AbstractInputStreamLookup inputStreamLookup; - - // NetLogin injector - private NetLoginInjector netLoginInjector; - - // The last successful player hook - private PlayerInjector lastSuccessfulHook; - - // Dummy injection - private Cache dummyInjectors = - CacheBuilder.newBuilder(). - expireAfterWrite(30, TimeUnit.SECONDS). - build(BlockingHashMap.newInvalidCacheLoader()); - - // Player injection - private Map playerInjection = Maps.newConcurrentMap(); - - // Player injection types - private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; - private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; - - // Error reporter - private ErrorReporter reporter; - - // Whether or not we're closing - private boolean hasClosed; - - // Used to invoke events - private ListenerInvoker invoker; - - // Current Minecraft version - private MinecraftVersion version; - - // Enabled packet filters - private IntegerSet sendingFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); - - // List of packet listeners - private Set packetListeners; - - // The class loader we're using - private ClassLoader classLoader; - - // Used to filter injection attempts - private Predicate injectionFilter; - - public ProxyPlayerInjectionHandler( - ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, - ListenerInvoker invoker, Set packetListeners, Server server, MinecraftVersion version) { - - this.classLoader = classLoader; - this.reporter = reporter; - this.invoker = invoker; - this.injectionFilter = injectionFilter; - this.packetListeners = packetListeners; - this.version = version; - - this.inputStreamLookup = InputStreamLookupBuilder.newBuilder(). - server(server). - reporter(reporter). - build(); - - // Create net login injectors and the server connection injector - this.netLoginInjector = new NetLoginInjector(reporter, server, this); - this.serverInjection = new InjectedServerConnection(reporter, inputStreamLookup, server, netLoginInjector); - serverInjection.injectList(); - } - - @Override - public void postWorldLoaded() { - // This will actually create a socket and a seperate thread ... - if (inputStreamLookup != null) { - inputStreamLookup.postWorldLoaded(); - } - } - - /** - * Retrieves how the server packets are read. - * @return Injection method for reading server packets. - */ - @Override - public PlayerInjectHooks getPlayerHook() { - return getPlayerHook(GamePhase.PLAYING); - } - - /** - * Retrieves how the server packets are read. - * @param phase - the current game phase. - * @return Injection method for reading server packets. - */ - @Override - public PlayerInjectHooks getPlayerHook(GamePhase phase) { - switch (phase) { - case LOGIN: - return loginPlayerHook; - case PLAYING: - return playingPlayerHook; - default: - throw new IllegalArgumentException("Cannot retrieve injection hook for both phases at the same time."); - } - } - - /** - * Sets how the server packets are read. - * @param playerHook - the new injection method for reading server packets. - */ - @Override - public void setPlayerHook(PlayerInjectHooks playerHook) { - setPlayerHook(GamePhase.PLAYING, playerHook); - } - - /** - * Sets how the server packets are read. - * @param phase - the current game phase. - * @param playerHook - the new injection method for reading server packets. - */ - @Override - public void setPlayerHook(GamePhase phase, PlayerInjectHooks playerHook) { - if (phase.hasLogin()) - loginPlayerHook = playerHook; - if (phase.hasPlaying()) - playingPlayerHook = playerHook; - - // Make sure the current listeners are compatible - checkListener(packetListeners); - } - - /** - * Add an underlying packet handler of the given ID. - * @param packetID - packet ID to register. - */ - @Override - public void addPacketHandler(int packetID) { - sendingFilters.add(packetID); - } - - /** - * Remove an underlying packet handler of ths ID. - * @param packetID - packet ID to unregister. - */ - @Override - public void removePacketHandler(int packetID) { - sendingFilters.remove(packetID); - } - - /** - * Used to construct a player hook. - * @param player - the player to hook. - * @param hook - the hook type. - * @return A new player hoook - * @throws IllegalAccessException Unable to do our reflection magic. - */ - private PlayerInjector getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException { - // Construct the correct player hook - switch (hook) { - case NETWORK_HANDLER_FIELDS: - return new NetworkFieldInjector(classLoader, reporter, player, invoker, sendingFilters); - case NETWORK_MANAGER_OBJECT: - return new NetworkObjectInjector(classLoader, reporter, player, invoker, sendingFilters); - case NETWORK_SERVER_OBJECT: - return new NetworkServerInjector(classLoader, reporter, player, invoker, sendingFilters, serverInjection); - default: - throw new IllegalArgumentException("Cannot construct a player injector."); - } - } - - /** - * Retrieve a player by its DataInput connection. - * @param inputStream - the associated DataInput connection. - * @return The player we found. - */ - @Override - public Player getPlayerByConnection(DataInputStream inputStream) { - // Wait until the connection owner has been established - SocketInjector injector = inputStreamLookup.waitSocketInjector(inputStream); - - if (injector != null) { - return injector.getPlayer(); - } else { - return null; - } - } - - /** - * Helper function that retrieves the injector type of a given player injector. - * @param injector - injector type. - * @return The injector type. - */ - private PlayerInjectHooks getInjectorType(PlayerInjector injector) { - return injector != null ? injector.getHookType() : PlayerInjectHooks.NONE; - } - - /** - * Initialize a player hook, allowing us to read server packets. - *

- * This call will be ignored if there's no listener that can receive the given events. - * @param player - player to hook. - * @param strategy - how to handle previous player injections. - */ - @Override - public void injectPlayer(Player player, ConflictStrategy strategy) { - // Inject using the player instance itself - if (isInjectionNecessary(GamePhase.PLAYING)) { - injectPlayer(player, player, strategy, GamePhase.PLAYING); - } - } - - /** - * Determine if it's truly necessary to perform the given player injection. - * @param phase - current game phase. - * @return TRUE if we should perform the injection, FALSE otherwise. - */ - public boolean isInjectionNecessary(GamePhase phase) { - return injectionFilter.apply(phase); - } - - /** - * Initialize a player hook, allowing us to read server packets. - *

- * This method will always perform the instructed injection. - * - * @param player - player to hook. - * @param injectionPoint - the object to use during the injection process. - * @param phase - the current game phase. - * @return The resulting player injector, or NULL if the injection failed. - */ - PlayerInjector injectPlayer(Player player, Object injectionPoint, ConflictStrategy stategy, GamePhase phase) { - if (player == null) - throw new IllegalArgumentException("Player cannot be NULL."); - if (injectionPoint == null) - throw new IllegalArgumentException("injectionPoint cannot be NULL."); - if (phase == null) - throw new IllegalArgumentException("phase cannot be NULL."); - - // Unfortunately, due to NetLoginHandler, multiple threads may potentially call this method. - synchronized (player) { - return injectPlayerInternal(player, injectionPoint, stategy, phase); - } - } - - // Unsafe variant of the above - private PlayerInjector injectPlayerInternal(Player player, Object injectionPoint, ConflictStrategy stategy, GamePhase phase) { - PlayerInjector injector = playerInjection.get(player); - PlayerInjectHooks tempHook = getPlayerHook(phase); - PlayerInjectHooks permanentHook = tempHook; - - // The given player object may be fake, so be careful! - - // See if we need to inject something else - boolean invalidInjector = injector != null ? !injector.canInject(phase) : true; - - // Don't inject if the class has closed - if (!hasClosed && (tempHook != getInjectorType(injector) || invalidInjector)) { - while (tempHook != PlayerInjectHooks.NONE) { - // Whether or not the current hook method failed completely - boolean hookFailed = false; - - // Remove the previous hook, if any - cleanupHook(injector); - - try { - injector = getHookInstance(player, tempHook); - - // Make sure this injection method supports the current game phase - if (injector.canInject(phase)) { - injector.initialize(injectionPoint); - - // Get socket and socket injector - SocketAddress address = injector.getAddress(); - SocketInjector previous = inputStreamLookup.peekSocketInjector(address); - - // Close any previously associated hooks before we proceed - if (previous != null && !(player instanceof Factory)) { - switch (stategy) { - case OVERRIDE: - uninjectPlayer(previous.getPlayer(), true); - break; - case BAIL_OUT: - return null; - } - } - injector.injectManager(); - - // Save injector - inputStreamLookup.setSocketInjector(address, injector); - break; - } - - } catch (PlayerLoggedOutException e) { - throw e; - - } catch (Exception e) { - // Mark this injection attempt as a failure - reporter.reportDetailed(this, "Player hook " + tempHook.toString() + " failed.", - e, player, injectionPoint, phase); - hookFailed = true; - } - - // Choose the previous player hook type - tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1]; - - if (hookFailed) - reporter.reportWarning(this, "Switching to " + tempHook.toString() + " instead."); - - // Check for UTTER FAILURE - if (tempHook == PlayerInjectHooks.NONE) { - cleanupHook(injector); - injector = null; - hookFailed = true; - } - - // Should we set the default hook method too? - if (hookFailed) { - permanentHook = tempHook; - } - } - - // Update values - if (injector != null) - lastSuccessfulHook = injector; - if (permanentHook != getPlayerHook(phase)) - setPlayerHook(phase, tempHook); - - // Save injector - if (injector != null) { - playerInjection.put(player, injector); - } - } - - return injector; - } - - private void cleanupHook(PlayerInjector injector) { - // Clean up as much as possible - try { - if (injector != null) - injector.cleanupAll(); - } catch (Exception ex) { - reporter.reportDetailed(this, "Cleaing up after player hook failed.", ex, injector); - } - } - - /** - * Invoke special routines for handling disconnect before a player is uninjected. - * @param player - player to process. - */ - @Override - public void handleDisconnect(Player player) { - PlayerInjector injector = getInjector(player); - - if (injector != null) { - injector.handleDisconnect(); - } - } - - @Override - public void updatePlayer(Player player) { - SocketInjector injector = inputStreamLookup.peekSocketInjector(player.getAddress()); - - if (injector != null) { - injector.setUpdatedPlayer(player); - } else { - inputStreamLookup.setSocketInjector(player.getAddress(), - new BukkitSocketInjector(player)); - } - } - - /** - * Unregisters the given player. - * @param player - player to unregister. - * @return TRUE if a player has been uninjected, FALSE otherwise. - */ - @Override - public boolean uninjectPlayer(Player player) { - return uninjectPlayer(player, false); - } - - /** - * Unregisters the given player. - * @param player - player to unregister. - * @param prepareNextHook - whether or not we need to fix any lingering hooks. - * @return TRUE if a player has been uninjected, FALSE otherwise. - */ - private boolean uninjectPlayer(Player player, boolean prepareNextHook) { - if (!hasClosed && player != null) { - - PlayerInjector injector = playerInjection.remove(player); - - if (injector != null) { - injector.cleanupAll(); - - // Remove the "hooked" network manager in our instance as well - if (prepareNextHook && injector instanceof NetworkObjectInjector) { - try { - PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); - dummyInjector.initializePlayer(player); - dummyInjector.setNetworkManager(injector.getNetworkManager(), true); - - } catch (IllegalAccessException e) { - // Let the user know - reporter.reportWarning(this, "Unable to fully revert old injector. May cause conflicts.", e); - } - } - - return true; - } - } - - return false; - } - - /** - * Unregisters a player by the given address. - *

- * If the server handler has been created before we've gotten a chance to unject the player, - * the method will try a workaround to remove the injected hook in the NetServerHandler. - * - * @param address - address of the player to unregister. - * @return TRUE if a player has been uninjected, FALSE otherwise. - */ - @Override - public boolean uninjectPlayer(InetSocketAddress address) { - if (!hasClosed && address != null) { - SocketInjector injector = inputStreamLookup.peekSocketInjector(address); - - // Clean up - if (injector != null) - uninjectPlayer(injector.getPlayer(), true); - return true; - } - - return false; - } - - /** - * Send the given packet to the given reciever. - * @param reciever - the player receiver. - * @param packet - the packet to send. - * @param filters - whether or not to invoke the packet filters. - * @throws InvocationTargetException If an error occured during sending. - */ - @Override - public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { - SocketInjector injector = getInjector(reciever); - - // Send the packet, or drop it completely - if (injector != null) { - injector.sendServerPacket(packet.getHandle(), filters); - } else { - throw new PlayerLoggedOutException(String.format( - "Unable to send packet %s (%s): Player %s has logged out.", - packet.getID(), packet, reciever.getName() - )); - } - } - - /** - * Recieve a packet as if it were sent by the given player. - * @param player - the sender. - * @param mcPacket - the packet to process. - * @throws IllegalAccessException If the reflection machinery failed. - * @throws InvocationTargetException If the underlying method caused an error. - */ - @Override - public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { - PlayerInjector injector = getInjector(player); - - // Process the given packet, or simply give up - if (injector != null) - injector.processPacket(mcPacket); - else - throw new PlayerLoggedOutException(String.format( - "Unable to receieve packet %s. Player %s has logged out.", - mcPacket, player.getName() - )); - } - - /** - * Retrieve the injector associated with this player. - * @param player - the player to find. - * @return The injector, or NULL if not found. - */ - private PlayerInjector getInjector(Player player) { - PlayerInjector injector = playerInjection.get(player); - - if (injector == null) { - // Try getting it from the player itself - SocketAddress address = player.getAddress(); - - // Must have logged out - there's nothing we can do - if (address == null) - return null; - - // Look that up without blocking - SocketInjector result = inputStreamLookup.peekSocketInjector(address); - - // Ensure that it is non-null and a player injector - if (result instanceof PlayerInjector) - return (PlayerInjector) result; - else - // Make a dummy injector them - return createDummyInjector(player); - - } else { - return injector; - } - } - - /** - * Construct a simple dummy injector incase none has been constructed. - * @param player - the CraftPlayer to construct for. - * @return A dummy injector, or NULL if the given player is not a CraftPlayer. - */ - private PlayerInjector createDummyInjector(Player player) { - if (!MinecraftReflection.getCraftPlayerClass().isAssignableFrom(player.getClass())) { - // No - this is not safe - return null; - } - - try { - PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); - dummyInjector.initializePlayer(player); - - // This probably means the player has disconnected - if (dummyInjector.getSocket() == null) { - return null; - } - - inputStreamLookup.setSocketInjector(dummyInjector.getAddress(), dummyInjector); - dummyInjectors.asMap().put(player, dummyInjector); - return dummyInjector; - - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access fields.", e); - } - } - - /** - * Retrieve a player injector by looking for its NetworkManager. - * @param networkManager - current network manager. - * @return Related player injector. - */ - PlayerInjector getInjectorByNetworkHandler(Object networkManager) { - // That's not legal - if (networkManager == null) - return null; - - // O(n) is okay in this instance. This is only a backup solution. - for (PlayerInjector injector : playerInjection.values()) { - if (injector.getNetworkManager() == networkManager) - return injector; - } - - // None found - return null; - } - - /** - * Determine if the given listeners are valid. - * @param listeners - listeners to check. - */ - @Override - public void checkListener(Set listeners) { - // Make sure the current listeners are compatible - if (lastSuccessfulHook != null) { - for (PacketListener listener : listeners) { - checkListener(listener); - } - } - } - - /** - * Determine if a listener is valid or not. - *

- * If not, a warning will be printed to the console. - * @param listener - listener to check. - */ - @Override - public void checkListener(PacketListener listener) { - if (lastSuccessfulHook != null) { - UnsupportedListener result = lastSuccessfulHook.checkListener(version, listener); - - // We won't prevent the listener, as it may still have valid packets - if (result != null) { - reporter.reportWarning(this, "Cannot fully register listener for " + - PacketAdapter.getPluginName(listener) + ": " + result.toString()); - - // These are illegal - for (int packetID : result.getPackets()) - removePacketHandler(packetID); - } - } - } - - /** - * Retrieve the current list of registered sending listeners. - * @return List of the sending listeners's packet IDs. - */ - @Override - public Set getSendingFilters() { - return sendingFilters.toSet(); - } - - @Override - public void close() { - // Guard - if (hasClosed || playerInjection == null) - return; - - // Remove everything - for (PlayerInjector injection : playerInjection.values()) { - if (injection != null) { - injection.cleanupAll(); - } - } - - // Remove server handler - if (inputStreamLookup != null) - inputStreamLookup.cleanupAll(); - if (serverInjection != null) - serverInjection.cleanupAll(); - if (netLoginInjector != null) - netLoginInjector.cleanupAll(); - inputStreamLookup = null; - serverInjection = null; - netLoginInjector = null; - hasClosed = true; - - playerInjection.clear(); - invoker = null; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.io.DataInputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import net.sf.cglib.proxy.Factory; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.concurrency.BlockingHashMap; +import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; +import com.comphenix.protocol.injector.server.BukkitSocketInjector; +import com.comphenix.protocol.injector.server.InputStreamLookupBuilder; +import com.comphenix.protocol.injector.server.SocketInjector; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; + +import com.google.common.base.Predicate; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Maps; + +/** + * Responsible for injecting into a player's sendPacket method. + * + * @author Kristian + */ +class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { + // Warnings and errors + public static final ReportType REPORT_UNSUPPPORTED_LISTENER = new ReportType("Cannot fully register listener for %s: %s"); + + // Fallback to older player hook types + public static final ReportType REPORT_PLAYER_HOOK_FAILED = new ReportType("Player hook %s failed."); + public static final ReportType REPORT_SWITCHED_PLAYER_HOOK = new ReportType("Switching to %s instead."); + + public static final ReportType REPORT_HOOK_CLEANUP_FAILED = new ReportType("Cleaing up after player hook failed."); + public static final ReportType REPORT_CANNOT_REVERT_HOOK = new ReportType("Unable to fully revert old injector. May cause conflicts."); + + // Server connection injection + private InjectedServerConnection serverInjection; + + // Server socket injection + private AbstractInputStreamLookup inputStreamLookup; + + // NetLogin injector + private NetLoginInjector netLoginInjector; + + // The last successful player hook + private PlayerInjector lastSuccessfulHook; + + // Dummy injection + private Cache dummyInjectors = + CacheBuilder.newBuilder(). + expireAfterWrite(30, TimeUnit.SECONDS). + build(BlockingHashMap.newInvalidCacheLoader()); + + // Player injection + private Map playerInjection = Maps.newConcurrentMap(); + + // Player injection types + private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; + private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; + + // Error reporter + private ErrorReporter reporter; + + // Whether or not we're closing + private boolean hasClosed; + + // Used to invoke events + private ListenerInvoker invoker; + + // Current Minecraft version + private MinecraftVersion version; + + // Enabled packet filters + private IntegerSet sendingFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + + // List of packet listeners + private Set packetListeners; + + // The class loader we're using + private ClassLoader classLoader; + + // Used to filter injection attempts + private Predicate injectionFilter; + + public ProxyPlayerInjectionHandler( + ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, + ListenerInvoker invoker, Set packetListeners, Server server, MinecraftVersion version) { + + this.classLoader = classLoader; + this.reporter = reporter; + this.invoker = invoker; + this.injectionFilter = injectionFilter; + this.packetListeners = packetListeners; + this.version = version; + + this.inputStreamLookup = InputStreamLookupBuilder.newBuilder(). + server(server). + reporter(reporter). + build(); + + // Create net login injectors and the server connection injector + this.netLoginInjector = new NetLoginInjector(reporter, server, this); + this.serverInjection = new InjectedServerConnection(reporter, inputStreamLookup, server, netLoginInjector); + serverInjection.injectList(); + } + + @Override + public void postWorldLoaded() { + // This will actually create a socket and a seperate thread ... + if (inputStreamLookup != null) { + inputStreamLookup.postWorldLoaded(); + } + } + + /** + * Retrieves how the server packets are read. + * @return Injection method for reading server packets. + */ + @Override + public PlayerInjectHooks getPlayerHook() { + return getPlayerHook(GamePhase.PLAYING); + } + + /** + * Retrieves how the server packets are read. + * @param phase - the current game phase. + * @return Injection method for reading server packets. + */ + @Override + public PlayerInjectHooks getPlayerHook(GamePhase phase) { + switch (phase) { + case LOGIN: + return loginPlayerHook; + case PLAYING: + return playingPlayerHook; + default: + throw new IllegalArgumentException("Cannot retrieve injection hook for both phases at the same time."); + } + } + + /** + * Sets how the server packets are read. + * @param playerHook - the new injection method for reading server packets. + */ + @Override + public void setPlayerHook(PlayerInjectHooks playerHook) { + setPlayerHook(GamePhase.PLAYING, playerHook); + } + + /** + * Sets how the server packets are read. + * @param phase - the current game phase. + * @param playerHook - the new injection method for reading server packets. + */ + @Override + public void setPlayerHook(GamePhase phase, PlayerInjectHooks playerHook) { + if (phase.hasLogin()) + loginPlayerHook = playerHook; + if (phase.hasPlaying()) + playingPlayerHook = playerHook; + + // Make sure the current listeners are compatible + checkListener(packetListeners); + } + + /** + * Add an underlying packet handler of the given ID. + * @param packetID - packet ID to register. + */ + @Override + public void addPacketHandler(int packetID) { + sendingFilters.add(packetID); + } + + /** + * Remove an underlying packet handler of ths ID. + * @param packetID - packet ID to unregister. + */ + @Override + public void removePacketHandler(int packetID) { + sendingFilters.remove(packetID); + } + + /** + * Used to construct a player hook. + * @param player - the player to hook. + * @param hook - the hook type. + * @return A new player hoook + * @throws IllegalAccessException Unable to do our reflection magic. + */ + private PlayerInjector getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException { + // Construct the correct player hook + switch (hook) { + case NETWORK_HANDLER_FIELDS: + return new NetworkFieldInjector(classLoader, reporter, player, invoker, sendingFilters); + case NETWORK_MANAGER_OBJECT: + return new NetworkObjectInjector(classLoader, reporter, player, invoker, sendingFilters); + case NETWORK_SERVER_OBJECT: + return new NetworkServerInjector(classLoader, reporter, player, invoker, sendingFilters, serverInjection); + default: + throw new IllegalArgumentException("Cannot construct a player injector."); + } + } + + /** + * Retrieve a player by its DataInput connection. + * @param inputStream - the associated DataInput connection. + * @return The player we found. + */ + @Override + public Player getPlayerByConnection(DataInputStream inputStream) { + // Wait until the connection owner has been established + SocketInjector injector = inputStreamLookup.waitSocketInjector(inputStream); + + if (injector != null) { + return injector.getPlayer(); + } else { + return null; + } + } + + /** + * Helper function that retrieves the injector type of a given player injector. + * @param injector - injector type. + * @return The injector type. + */ + private PlayerInjectHooks getInjectorType(PlayerInjector injector) { + return injector != null ? injector.getHookType() : PlayerInjectHooks.NONE; + } + + /** + * Initialize a player hook, allowing us to read server packets. + *

+ * This call will be ignored if there's no listener that can receive the given events. + * @param player - player to hook. + * @param strategy - how to handle previous player injections. + */ + @Override + public void injectPlayer(Player player, ConflictStrategy strategy) { + // Inject using the player instance itself + if (isInjectionNecessary(GamePhase.PLAYING)) { + injectPlayer(player, player, strategy, GamePhase.PLAYING); + } + } + + /** + * Determine if it's truly necessary to perform the given player injection. + * @param phase - current game phase. + * @return TRUE if we should perform the injection, FALSE otherwise. + */ + public boolean isInjectionNecessary(GamePhase phase) { + return injectionFilter.apply(phase); + } + + /** + * Initialize a player hook, allowing us to read server packets. + *

+ * This method will always perform the instructed injection. + * + * @param player - player to hook. + * @param injectionPoint - the object to use during the injection process. + * @param phase - the current game phase. + * @return The resulting player injector, or NULL if the injection failed. + */ + PlayerInjector injectPlayer(Player player, Object injectionPoint, ConflictStrategy stategy, GamePhase phase) { + if (player == null) + throw new IllegalArgumentException("Player cannot be NULL."); + if (injectionPoint == null) + throw new IllegalArgumentException("injectionPoint cannot be NULL."); + if (phase == null) + throw new IllegalArgumentException("phase cannot be NULL."); + + // Unfortunately, due to NetLoginHandler, multiple threads may potentially call this method. + synchronized (player) { + return injectPlayerInternal(player, injectionPoint, stategy, phase); + } + } + + // Unsafe variant of the above + private PlayerInjector injectPlayerInternal(Player player, Object injectionPoint, ConflictStrategy stategy, GamePhase phase) { + PlayerInjector injector = playerInjection.get(player); + PlayerInjectHooks tempHook = getPlayerHook(phase); + PlayerInjectHooks permanentHook = tempHook; + + // The given player object may be fake, so be careful! + + // See if we need to inject something else + boolean invalidInjector = injector != null ? !injector.canInject(phase) : true; + + // Don't inject if the class has closed + if (!hasClosed && (tempHook != getInjectorType(injector) || invalidInjector)) { + while (tempHook != PlayerInjectHooks.NONE) { + // Whether or not the current hook method failed completely + boolean hookFailed = false; + + // Remove the previous hook, if any + cleanupHook(injector); + + try { + injector = getHookInstance(player, tempHook); + + // Make sure this injection method supports the current game phase + if (injector.canInject(phase)) { + injector.initialize(injectionPoint); + + // Get socket and socket injector + SocketAddress address = injector.getAddress(); + SocketInjector previous = inputStreamLookup.peekSocketInjector(address); + + // Close any previously associated hooks before we proceed + if (previous != null && !(player instanceof Factory)) { + switch (stategy) { + case OVERRIDE: + uninjectPlayer(previous.getPlayer(), true); + break; + case BAIL_OUT: + return null; + } + } + injector.injectManager(); + + // Save injector + inputStreamLookup.setSocketInjector(address, injector); + break; + } + + } catch (PlayerLoggedOutException e) { + throw e; + + } catch (Exception e) { + // Mark this injection attempt as a failure + reporter.reportDetailed(this, + Report.newBuilder(REPORT_PLAYER_HOOK_FAILED).messageParam(tempHook).callerParam(player, injectionPoint, phase).error(e) + ); + hookFailed = true; + } + + // Choose the previous player hook type + tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1]; + + if (hookFailed) + reporter.reportWarning(this, Report.newBuilder(REPORT_SWITCHED_PLAYER_HOOK).messageParam(tempHook)); + + // Check for UTTER FAILURE + if (tempHook == PlayerInjectHooks.NONE) { + cleanupHook(injector); + injector = null; + hookFailed = true; + } + + // Should we set the default hook method too? + if (hookFailed) { + permanentHook = tempHook; + } + } + + // Update values + if (injector != null) + lastSuccessfulHook = injector; + if (permanentHook != getPlayerHook(phase)) + setPlayerHook(phase, tempHook); + + // Save injector + if (injector != null) { + playerInjection.put(player, injector); + } + } + + return injector; + } + + private void cleanupHook(PlayerInjector injector) { + // Clean up as much as possible + try { + if (injector != null) + injector.cleanupAll(); + } catch (Exception ex) { + reporter.reportDetailed(this, Report.newBuilder(REPORT_HOOK_CLEANUP_FAILED).callerParam(injector).error(ex)); + } + } + + /** + * Invoke special routines for handling disconnect before a player is uninjected. + * @param player - player to process. + */ + @Override + public void handleDisconnect(Player player) { + PlayerInjector injector = getInjector(player); + + if (injector != null) { + injector.handleDisconnect(); + } + } + + @Override + public void updatePlayer(Player player) { + SocketInjector injector = inputStreamLookup.peekSocketInjector(player.getAddress()); + + if (injector != null) { + injector.setUpdatedPlayer(player); + } else { + inputStreamLookup.setSocketInjector(player.getAddress(), + new BukkitSocketInjector(player)); + } + } + + /** + * Unregisters the given player. + * @param player - player to unregister. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + @Override + public boolean uninjectPlayer(Player player) { + return uninjectPlayer(player, false); + } + + /** + * Unregisters the given player. + * @param player - player to unregister. + * @param prepareNextHook - whether or not we need to fix any lingering hooks. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + private boolean uninjectPlayer(Player player, boolean prepareNextHook) { + if (!hasClosed && player != null) { + + PlayerInjector injector = playerInjection.remove(player); + + if (injector != null) { + injector.cleanupAll(); + + // Remove the "hooked" network manager in our instance as well + if (prepareNextHook && injector instanceof NetworkObjectInjector) { + try { + PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); + dummyInjector.initializePlayer(player); + dummyInjector.setNetworkManager(injector.getNetworkManager(), true); + + } catch (IllegalAccessException e) { + // Let the user know + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_REVERT_HOOK).error(e)); + } + } + + return true; + } + } + + return false; + } + + /** + * Unregisters a player by the given address. + *

+ * If the server handler has been created before we've gotten a chance to unject the player, + * the method will try a workaround to remove the injected hook in the NetServerHandler. + * + * @param address - address of the player to unregister. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + @Override + public boolean uninjectPlayer(InetSocketAddress address) { + if (!hasClosed && address != null) { + SocketInjector injector = inputStreamLookup.peekSocketInjector(address); + + // Clean up + if (injector != null) + uninjectPlayer(injector.getPlayer(), true); + return true; + } + + return false; + } + + /** + * Send the given packet to the given reciever. + * @param reciever - the player receiver. + * @param packet - the packet to send. + * @param filters - whether or not to invoke the packet filters. + * @throws InvocationTargetException If an error occured during sending. + */ + @Override + public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { + SocketInjector injector = getInjector(reciever); + + // Send the packet, or drop it completely + if (injector != null) { + injector.sendServerPacket(packet.getHandle(), filters); + } else { + throw new PlayerLoggedOutException(String.format( + "Unable to send packet %s (%s): Player %s has logged out.", + packet.getID(), packet, reciever.getName() + )); + } + } + + /** + * Recieve a packet as if it were sent by the given player. + * @param player - the sender. + * @param mcPacket - the packet to process. + * @throws IllegalAccessException If the reflection machinery failed. + * @throws InvocationTargetException If the underlying method caused an error. + */ + @Override + public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + PlayerInjector injector = getInjector(player); + + // Process the given packet, or simply give up + if (injector != null) + injector.processPacket(mcPacket); + else + throw new PlayerLoggedOutException(String.format( + "Unable to receieve packet %s. Player %s has logged out.", + mcPacket, player.getName() + )); + } + + /** + * Retrieve the injector associated with this player. + * @param player - the player to find. + * @return The injector, or NULL if not found. + */ + private PlayerInjector getInjector(Player player) { + PlayerInjector injector = playerInjection.get(player); + + if (injector == null) { + // Try getting it from the player itself + SocketAddress address = player.getAddress(); + + // Must have logged out - there's nothing we can do + if (address == null) + return null; + + // Look that up without blocking + SocketInjector result = inputStreamLookup.peekSocketInjector(address); + + // Ensure that it is non-null and a player injector + if (result instanceof PlayerInjector) + return (PlayerInjector) result; + else + // Make a dummy injector them + return createDummyInjector(player); + + } else { + return injector; + } + } + + /** + * Construct a simple dummy injector incase none has been constructed. + * @param player - the CraftPlayer to construct for. + * @return A dummy injector, or NULL if the given player is not a CraftPlayer. + */ + private PlayerInjector createDummyInjector(Player player) { + if (!MinecraftReflection.getCraftPlayerClass().isAssignableFrom(player.getClass())) { + // No - this is not safe + return null; + } + + try { + PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); + dummyInjector.initializePlayer(player); + + // This probably means the player has disconnected + if (dummyInjector.getSocket() == null) { + return null; + } + + inputStreamLookup.setSocketInjector(dummyInjector.getAddress(), dummyInjector); + dummyInjectors.asMap().put(player, dummyInjector); + return dummyInjector; + + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access fields.", e); + } + } + + /** + * Retrieve a player injector by looking for its NetworkManager. + * @param networkManager - current network manager. + * @return Related player injector. + */ + PlayerInjector getInjectorByNetworkHandler(Object networkManager) { + // That's not legal + if (networkManager == null) + return null; + + // O(n) is okay in this instance. This is only a backup solution. + for (PlayerInjector injector : playerInjection.values()) { + if (injector.getNetworkManager() == networkManager) + return injector; + } + + // None found + return null; + } + + /** + * Determine if the given listeners are valid. + * @param listeners - listeners to check. + */ + @Override + public void checkListener(Set listeners) { + // Make sure the current listeners are compatible + if (lastSuccessfulHook != null) { + for (PacketListener listener : listeners) { + checkListener(listener); + } + } + } + + /** + * Determine if a listener is valid or not. + *

+ * If not, a warning will be printed to the console. + * @param listener - listener to check. + */ + @Override + public void checkListener(PacketListener listener) { + if (lastSuccessfulHook != null) { + UnsupportedListener result = lastSuccessfulHook.checkListener(version, listener); + + // We won't prevent the listener, as it may still have valid packets + if (result != null) { + reporter.reportWarning(this, + Report.newBuilder(REPORT_UNSUPPPORTED_LISTENER).messageParam(PacketAdapter.getPluginName(listener), result) + ); + + // These are illegal + for (int packetID : result.getPackets()) + removePacketHandler(packetID); + } + } + } + + /** + * Retrieve the current list of registered sending listeners. + * @return List of the sending listeners's packet IDs. + */ + @Override + public Set getSendingFilters() { + return sendingFilters.toSet(); + } + + @Override + public void close() { + // Guard + if (hasClosed || playerInjection == null) + return; + + // Remove everything + for (PlayerInjector injection : playerInjection.values()) { + if (injection != null) { + injection.cleanupAll(); + } + } + + // Remove server handler + if (inputStreamLookup != null) + inputStreamLookup.cleanupAll(); + if (serverInjection != null) + serverInjection.cleanupAll(); + if (netLoginInjector != null) + netLoginInjector.cleanupAll(); + inputStreamLookup = null; + serverInjection = null; + netLoginInjector = null; + hasClosed = true; + + playerInjection.clear(); + invoker = null; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java index 26c937ea..6653916d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -1,365 +1,370 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.reflect.compiler; - -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryPoolMXBean; -import java.lang.management.MemoryUsage; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.reflect.StructureModifier; -import com.comphenix.protocol.reflect.compiler.StructureCompiler.StructureKey; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.util.concurrent.ThreadFactoryBuilder; - -/** - * Compiles structure modifiers on a background thread. - *

- * This is necessary as we cannot block the main thread. - * - * @author Kristian - */ -public class BackgroundCompiler { - - /** - * The default format for the name of new worker threads. - */ - public static final String THREAD_FORMAT = "ProtocolLib-StructureCompiler %s"; - - // How long to wait for a shutdown - public static final int SHUTDOWN_DELAY_MS = 2000; - - /** - * The default fraction of perm gen space after which the background compiler will be disabled. - */ - public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65; - - // The single background compiler we're using - private static BackgroundCompiler backgroundCompiler; - - // Classes we're currently compiling - private Map>> listeners = Maps.newHashMap(); - private Object listenerLock = new Object(); - - private StructureCompiler compiler; - private boolean enabled; - private boolean shuttingDown; - - private ExecutorService executor; - private ErrorReporter reporter; - - private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN; - - /** - * Retrieves the current background compiler. - * @return Current background compiler. - */ - public static BackgroundCompiler getInstance() { - return backgroundCompiler; - } - - /** - * Sets the single background compiler we're using. - * @param backgroundCompiler - current background compiler, or NULL if the library is not loaded. - */ - public static void setInstance(BackgroundCompiler backgroundCompiler) { - BackgroundCompiler.backgroundCompiler = backgroundCompiler; - } - - /** - * Initialize a background compiler. - *

- * Uses the default {@link #THREAD_FORMAT} to name worker threads. - * @param loader - class loader from Bukkit. - * @param reporter - current error reporter. - */ - public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter) { - ThreadFactory factory = new ThreadFactoryBuilder(). - setDaemon(true). - setNameFormat(THREAD_FORMAT). - build(); - initializeCompiler(loader, reporter, Executors.newSingleThreadExecutor(factory)); - } - - /** - * Initialize a background compiler utilizing the given thread pool. - * @param loader - class loader from Bukkit. - * @param reporter - current error reporter. - * @param executor - thread pool we'll use. - */ - public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) { - initializeCompiler(loader, reporter, executor); - } - - // Avoid "Constructor call must be the first statement". - private void initializeCompiler(ClassLoader loader, @Nullable ErrorReporter reporter, ExecutorService executor) { - if (loader == null) - throw new IllegalArgumentException("loader cannot be NULL"); - if (executor == null) - throw new IllegalArgumentException("executor cannot be NULL"); - - this.compiler = new StructureCompiler(loader); - this.reporter = reporter; - this.executor = executor; - this.enabled = true; - } - - /** - * Ensure that the indirectly given structure modifier is eventually compiled. - * @param cache - store of structure modifiers. - * @param key - key of the structure modifier to compile. - */ - @SuppressWarnings("rawtypes") - public void scheduleCompilation(final Map cache, final Class key) { - - @SuppressWarnings("unchecked") - final StructureModifier uncompiled = cache.get(key); - - if (uncompiled != null) { - scheduleCompilation(uncompiled, new CompileListener() { - @Override - public void onCompiled(StructureModifier compiledModifier) { - // Update cache - cache.put(key, compiledModifier); - } - }); - } - } - - /** - * Ensure that the given structure modifier is eventually compiled. - * @param uncompiled - structure modifier to compile. - * @param listener - listener responsible for responding to the compilation. - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - public void scheduleCompilation(final StructureModifier uncompiled, final CompileListener listener) { - // Only schedule if we're enabled - if (enabled && !shuttingDown) { - // Check perm gen - if (getPermGenUsage() > disablePermGenFraction) - return; - - // Don't try to schedule anything - if (executor == null || executor.isShutdown()) - return; - - // Use to look up structure modifiers - final StructureKey key = new StructureKey(uncompiled); - - // Allow others to listen in too - synchronized (listenerLock) { - List list = listeners.get(key); - - if (!listeners.containsKey(key)) { - listeners.put(key, (List) Lists.newArrayList(listener)); - } else { - // We're currently compiling - list.add(listener); - return; - } - } - - // Create the worker that will compile our modifier - Callable worker = new Callable() { - @Override - public Object call() throws Exception { - StructureModifier modifier = uncompiled; - List list = null; - - // Do our compilation - try { - modifier = compiler.compile(modifier); - - synchronized (listenerLock) { - list = listeners.get(key); - - // Prevent ConcurrentModificationExceptions - if (list != null) { - list = Lists.newArrayList(list); - } - } - - // Only execute the listeners if there is a list - if (list != null) { - for (Object compileListener : list) { - ((CompileListener) compileListener).onCompiled(modifier); - } - - // Remove it when we're done - synchronized (listenerLock) { - list = listeners.remove(key); - } - } - - } catch (Throwable e) { - // Disable future compilations! - setEnabled(false); - - // Inform about this error as best as we can - if (reporter != null) { - reporter.reportDetailed(BackgroundCompiler.this, - "Cannot compile structure. Disabing compiler.", e, uncompiled); - } else { - System.err.println("Exception occured in structure compiler: "); - e.printStackTrace(); - } - } - - // We'll also return the new structure modifier - return modifier; - - } - }; - - try { - // Lookup the previous class name on the main thread. - // This is necessary as the Bukkit class loaders are not thread safe - if (compiler.lookupClassLoader(uncompiled)) { - try { - worker.call(); - } catch (Exception e) { - // Impossible! - e.printStackTrace(); - } - - } else { - - // Perform the compilation on a seperate thread - executor.submit(worker); - } - - } catch (RejectedExecutionException e) { - // Occures when the underlying queue is overflowing. Since the compilation - // is only an optmization and not really essential we'll just log this failure - // and move on. - reporter.reportWarning(this, "Unable to schedule compilation task.", e); - } - } - } - - /** - * Add a compile listener if we are still waiting for the structure modifier to be compiled. - * @param uncompiled - the structure modifier that may get compiled. - * @param listener - the listener to invoke in that case. - */ - @SuppressWarnings("unchecked") - public void addListener(final StructureModifier uncompiled, final CompileListener listener) { - synchronized (listenerLock) { - StructureKey key = new StructureKey(uncompiled); - - @SuppressWarnings("rawtypes") - List list = listeners.get(key); - - if (list != null) { - list.add(listener); - } - } - } - - /** - * Retrieve the current usage of the Perm Gen space in percentage. - * @return Usage of the perm gen space. - */ - private double getPermGenUsage() { - for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) { - if (item.getName().contains("Perm Gen")) { - MemoryUsage usage = item.getUsage(); - return usage.getUsed() / (double) usage.getCommitted(); - } - } - - // Unknown - return 0; - } - - /** - * Clean up after ourselves using the default timeout. - */ - public void shutdownAll() { - shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS); - } - - /** - * Clean up after ourselves. - * @param timeout - the maximum time to wait. - * @param unit - the time unit of the timeout argument. - */ - public void shutdownAll(long timeout, TimeUnit unit) { - setEnabled(false); - shuttingDown = true; - executor.shutdown(); - - try { - executor.awaitTermination(timeout, unit); - } catch (InterruptedException e) { - // Unlikely to ever occur - it's the main thread - e.printStackTrace(); - } - } - - /** - * Retrieve whether or not the background compiler is enabled. - * @return TRUE if it is enabled, FALSE otherwise. - */ - public boolean isEnabled() { - return enabled; - } - - /** - * Sets whether or not the background compiler is enabled. - * @param enabled - TRUE to enable it, FALSE otherwise. - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * Retrieve the fraction of perm gen space used after which the background compiler will be disabled. - * @return The fraction after which the background compiler is disabled. - */ - public double getDisablePermGenFraction() { - return disablePermGenFraction; - } - - /** - * Set the fraction of perm gen space used after which the background compiler will be disabled. - * @param fraction - the maximum use of perm gen space. - */ - public void setDisablePermGenFraction(double fraction) { - this.disablePermGenFraction = fraction; - } - - /** - * Retrieve the current structure compiler. - * @return Current structure compiler. - */ - public StructureCompiler getCompiler() { - return compiler; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.reflect.compiler; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.compiler.StructureCompiler.StructureKey; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +/** + * Compiles structure modifiers on a background thread. + *

+ * This is necessary as we cannot block the main thread. + * + * @author Kristian + */ +public class BackgroundCompiler { + public static final ReportType REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER = new ReportType("Cannot compile structure. Disabing compiler."); + public static final ReportType REPORT_CANNOT_SCHEDULE_COMPILATION = new ReportType("Unable to schedule compilation task."); + + /** + * The default format for the name of new worker threads. + */ + public static final String THREAD_FORMAT = "ProtocolLib-StructureCompiler %s"; + + // How long to wait for a shutdown + public static final int SHUTDOWN_DELAY_MS = 2000; + + /** + * The default fraction of perm gen space after which the background compiler will be disabled. + */ + public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65; + + // The single background compiler we're using + private static BackgroundCompiler backgroundCompiler; + + // Classes we're currently compiling + private Map>> listeners = Maps.newHashMap(); + private Object listenerLock = new Object(); + + private StructureCompiler compiler; + private boolean enabled; + private boolean shuttingDown; + + private ExecutorService executor; + private ErrorReporter reporter; + + private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN; + + /** + * Retrieves the current background compiler. + * @return Current background compiler. + */ + public static BackgroundCompiler getInstance() { + return backgroundCompiler; + } + + /** + * Sets the single background compiler we're using. + * @param backgroundCompiler - current background compiler, or NULL if the library is not loaded. + */ + public static void setInstance(BackgroundCompiler backgroundCompiler) { + BackgroundCompiler.backgroundCompiler = backgroundCompiler; + } + + /** + * Initialize a background compiler. + *

+ * Uses the default {@link #THREAD_FORMAT} to name worker threads. + * @param loader - class loader from Bukkit. + * @param reporter - current error reporter. + */ + public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter) { + ThreadFactory factory = new ThreadFactoryBuilder(). + setDaemon(true). + setNameFormat(THREAD_FORMAT). + build(); + initializeCompiler(loader, reporter, Executors.newSingleThreadExecutor(factory)); + } + + /** + * Initialize a background compiler utilizing the given thread pool. + * @param loader - class loader from Bukkit. + * @param reporter - current error reporter. + * @param executor - thread pool we'll use. + */ + public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) { + initializeCompiler(loader, reporter, executor); + } + + // Avoid "Constructor call must be the first statement". + private void initializeCompiler(ClassLoader loader, @Nullable ErrorReporter reporter, ExecutorService executor) { + if (loader == null) + throw new IllegalArgumentException("loader cannot be NULL"); + if (executor == null) + throw new IllegalArgumentException("executor cannot be NULL"); + + this.compiler = new StructureCompiler(loader); + this.reporter = reporter; + this.executor = executor; + this.enabled = true; + } + + /** + * Ensure that the indirectly given structure modifier is eventually compiled. + * @param cache - store of structure modifiers. + * @param key - key of the structure modifier to compile. + */ + @SuppressWarnings("rawtypes") + public void scheduleCompilation(final Map cache, final Class key) { + + @SuppressWarnings("unchecked") + final StructureModifier uncompiled = cache.get(key); + + if (uncompiled != null) { + scheduleCompilation(uncompiled, new CompileListener() { + @Override + public void onCompiled(StructureModifier compiledModifier) { + // Update cache + cache.put(key, compiledModifier); + } + }); + } + } + + /** + * Ensure that the given structure modifier is eventually compiled. + * @param uncompiled - structure modifier to compile. + * @param listener - listener responsible for responding to the compilation. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public void scheduleCompilation(final StructureModifier uncompiled, final CompileListener listener) { + // Only schedule if we're enabled + if (enabled && !shuttingDown) { + // Check perm gen + if (getPermGenUsage() > disablePermGenFraction) + return; + + // Don't try to schedule anything + if (executor == null || executor.isShutdown()) + return; + + // Use to look up structure modifiers + final StructureKey key = new StructureKey(uncompiled); + + // Allow others to listen in too + synchronized (listenerLock) { + List list = listeners.get(key); + + if (!listeners.containsKey(key)) { + listeners.put(key, (List) Lists.newArrayList(listener)); + } else { + // We're currently compiling + list.add(listener); + return; + } + } + + // Create the worker that will compile our modifier + Callable worker = new Callable() { + @Override + public Object call() throws Exception { + StructureModifier modifier = uncompiled; + List list = null; + + // Do our compilation + try { + modifier = compiler.compile(modifier); + + synchronized (listenerLock) { + list = listeners.get(key); + + // Prevent ConcurrentModificationExceptions + if (list != null) { + list = Lists.newArrayList(list); + } + } + + // Only execute the listeners if there is a list + if (list != null) { + for (Object compileListener : list) { + ((CompileListener) compileListener).onCompiled(modifier); + } + + // Remove it when we're done + synchronized (listenerLock) { + list = listeners.remove(key); + } + } + + } catch (Throwable e) { + // Disable future compilations! + setEnabled(false); + + // Inform about this error as best as we can + if (reporter != null) { + reporter.reportDetailed(BackgroundCompiler.this, + Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e) + ); + } else { + System.err.println("Exception occured in structure compiler: "); + e.printStackTrace(); + } + } + + // We'll also return the new structure modifier + return modifier; + + } + }; + + try { + // Lookup the previous class name on the main thread. + // This is necessary as the Bukkit class loaders are not thread safe + if (compiler.lookupClassLoader(uncompiled)) { + try { + worker.call(); + } catch (Exception e) { + // Impossible! + e.printStackTrace(); + } + + } else { + + // Perform the compilation on a seperate thread + executor.submit(worker); + } + + } catch (RejectedExecutionException e) { + // Occures when the underlying queue is overflowing. Since the compilation + // is only an optmization and not really essential we'll just log this failure + // and move on. + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_SCHEDULE_COMPILATION).error(e)); + } + } + } + + /** + * Add a compile listener if we are still waiting for the structure modifier to be compiled. + * @param uncompiled - the structure modifier that may get compiled. + * @param listener - the listener to invoke in that case. + */ + @SuppressWarnings("unchecked") + public void addListener(final StructureModifier uncompiled, final CompileListener listener) { + synchronized (listenerLock) { + StructureKey key = new StructureKey(uncompiled); + + @SuppressWarnings("rawtypes") + List list = listeners.get(key); + + if (list != null) { + list.add(listener); + } + } + } + + /** + * Retrieve the current usage of the Perm Gen space in percentage. + * @return Usage of the perm gen space. + */ + private double getPermGenUsage() { + for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) { + if (item.getName().contains("Perm Gen")) { + MemoryUsage usage = item.getUsage(); + return usage.getUsed() / (double) usage.getCommitted(); + } + } + + // Unknown + return 0; + } + + /** + * Clean up after ourselves using the default timeout. + */ + public void shutdownAll() { + shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS); + } + + /** + * Clean up after ourselves. + * @param timeout - the maximum time to wait. + * @param unit - the time unit of the timeout argument. + */ + public void shutdownAll(long timeout, TimeUnit unit) { + setEnabled(false); + shuttingDown = true; + executor.shutdown(); + + try { + executor.awaitTermination(timeout, unit); + } catch (InterruptedException e) { + // Unlikely to ever occur - it's the main thread + e.printStackTrace(); + } + } + + /** + * Retrieve whether or not the background compiler is enabled. + * @return TRUE if it is enabled, FALSE otherwise. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether or not the background compiler is enabled. + * @param enabled - TRUE to enable it, FALSE otherwise. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Retrieve the fraction of perm gen space used after which the background compiler will be disabled. + * @return The fraction after which the background compiler is disabled. + */ + public double getDisablePermGenFraction() { + return disablePermGenFraction; + } + + /** + * Set the fraction of perm gen space used after which the background compiler will be disabled. + * @param fraction - the maximum use of perm gen space. + */ + public void setDisablePermGenFraction(double fraction) { + this.disablePermGenFraction = fraction; + } + + /** + * Retrieve the current structure compiler. + * @return Current structure compiler. + */ + public StructureCompiler getCompiler() { + return compiler; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java index 4a4eedd3..70dbe262 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java @@ -1,529 +1,533 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.reflect.compiler; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.reflect.StructureModifier; -import com.google.common.base.Objects; -import com.google.common.primitives.Primitives; - -import net.sf.cglib.asm.*; - -// public class CompiledStructureModifierPacket20 extends CompiledStructureModifier { -// -// private Packet20NamedEntitySpawn typedTarget; -// -// public CompiledStructureModifierPacket20(StructureModifier other, StructureCompiler compiler) { -// super(); -// initialize(other); -// this.target = other.getTarget(); -// this.typedTarget = (Packet20NamedEntitySpawn) target; -// this.compiler = compiler; -// } -// -// @Override -// protected Object readGenerated(int fieldIndex) throws FieldAccessException { -// -// Packet20NamedEntitySpawn target = typedTarget; -// -// switch (fieldIndex) { -// case 0: return (Object) target.a; -// case 1: return (Object) target.b; -// case 2: return (Object) target.c; -// case 3: return super.readReflected(fieldIndex); -// case 4: return super.readReflected(fieldIndex); -// case 5: return (Object) target.f; -// case 6: return (Object) target.g; -// case 7: return (Object) target.h; -// default: -// throw new FieldAccessException("Invalid index " + fieldIndex); -// } -// } -// -// @Override -// protected StructureModifier writeGenerated(int index, Object value) throws FieldAccessException { -// -// Packet20NamedEntitySpawn target = typedTarget; -// -// switch (index) { -// case 0: target.a = (Integer) value; break; -// case 1: target.b = (String) value; break; -// case 2: target.c = (Integer) value; break; -// case 3: target.d = (Integer) value; break; -// case 4: super.writeReflected(index, value); break; -// case 5: super.writeReflected(index, value); break; -// case 6: target.g = (Byte) value; break; -// case 7: target.h = (Integer) value; break; -// default: -// throw new FieldAccessException("Invalid index " + index); -// } -// -// // Chaining -// return this; -// } -// } - -/** - * Represents a StructureModifier compiler. - * - * @author Kristian - */ -public final class StructureCompiler { - - // Used to store generated classes of different types - @SuppressWarnings("rawtypes") - static class StructureKey { - private Class targetType; - private Class fieldType; - - public StructureKey(StructureModifier source) { - this(source.getTargetType(), source.getFieldType()); - } - - public StructureKey(Class targetType, Class fieldType) { - this.targetType = targetType; - this.fieldType = fieldType; - } - - @Override - public int hashCode() { - return Objects.hashCode(targetType, fieldType); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof StructureKey) { - StructureKey other = (StructureKey) obj; - return Objects.equal(targetType, other.targetType) && - Objects.equal(fieldType, other.fieldType); - } - return false; - } - } - - // Used to load classes - private volatile static Method defineMethod; - - @SuppressWarnings("rawtypes") - private Map compiledCache = new ConcurrentHashMap(); - - // The class loader we'll store our classes - private ClassLoader loader; - - // References to other classes - private static String PACKAGE_NAME = "com/comphenix/protocol/reflect/compiler"; - private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier"; - private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier"; - private static String FIELD_EXCEPTION_CLASS = "com/comphenix/protocol/reflect/FieldAccessException"; - - /** - * Construct a structure compiler. - * @param loader - main class loader. - */ - StructureCompiler(ClassLoader loader) { - this.loader = loader; - } - - /** - * Lookup the current class loader for any previously generated classes before we attempt to generate something. - * @param source - the structure modifier to look up. - * @return TRUE if we successfully found a previously generated class, FALSE otherwise. - */ - public boolean lookupClassLoader(StructureModifier source) { - StructureKey key = new StructureKey(source); - - // See if there's a need to lookup the class name - if (compiledCache.containsKey(key)) { - return true; - } - - try { - String className = getCompiledName(source); - - // This class might have been generated before. Try to load it. - Class before = loader.loadClass(PACKAGE_NAME.replace('/', '.') + "." + className); - - if (before != null) { - compiledCache.put(key, before); - return true; - } - } catch (ClassNotFoundException e) { - // That's ok. - } - - // We need to compile the class - return false; - } - - /** - * Compiles the given structure modifier. - *

- * WARNING: Do NOT call this method in the main thread. Compiling may easily take 10 ms, which is already - * over 1/4 of a tick (50 ms). Let the background thread automatically compile the structure modifiers instead. - * @param source - structure modifier to compile. - * @return A compiled structure modifier. - */ - @SuppressWarnings("unchecked") - public synchronized StructureModifier compile(StructureModifier source) { - - // We cannot optimize a structure modifier with no public fields - if (!isAnyPublic(source.getFields())) { - return source; - } - - StructureKey key = new StructureKey(source); - Class compiledClass = compiledCache.get(key); - - if (!compiledCache.containsKey(key)) { - compiledClass = generateClass(source); - compiledCache.put(key, compiledClass); - } - - // Next, create an instance of this class - try { - return (StructureModifier) compiledClass.getConstructor( - StructureModifier.class, StructureCompiler.class). - newInstance(source, this); - } catch (OutOfMemoryError e) { - // Print the number of generated classes by the current instances - ProtocolLibrary.getErrorReporter().reportWarning( - this, "May have generated too many classes (count: " + compiledCache.size() + ")"); - throw e; - } catch (IllegalArgumentException e) { - throw new IllegalStateException("Used invalid parameters in instance creation", e); - } catch (SecurityException e) { - throw new RuntimeException("Security limitation!", e); - } catch (InstantiationException e) { - throw new RuntimeException("Error occured while instancing generated class.", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Security limitation! Cannot create instance of dynamic class.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Error occured while instancing generated class.", e); - } catch (NoSuchMethodException e) { - throw new IllegalStateException("Cannot happen.", e); - } - } - - /** - * Retrieve a variable identifier that can uniquely represent the given type. - * @param type - a type. - * @return A unique and legal identifier for the given type. - */ - private String getSafeTypeName(Class type) { - return type.getCanonicalName().replace("[]", "Array").replace(".", "_"); - } - - /** - * Retrieve the compiled name of a given structure modifier. - * @param source - the structure modifier. - * @return The unique, compiled name of a compiled structure modifier. - */ - private String getCompiledName(StructureModifier source) { - Class targetType = source.getTargetType(); - - // Concat class and field type - return "CompiledStructure$" + - getSafeTypeName(targetType) + "$" + - getSafeTypeName(source.getFieldType()); - } - - /** - * Compile a structure modifier. - * @param source - structure modifier. - * @return The compiled structure modifier. - */ - private Class generateClass(StructureModifier source) { - - ClassWriter cw = new ClassWriter(0); - Class targetType = source.getTargetType(); - - String className = getCompiledName(source); - String targetSignature = Type.getDescriptor(targetType); - String targetName = targetType.getName().replace('.', '/'); - - // Define class - cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className, - null, COMPILED_CLASS, null); - - createFields(cw, targetSignature); - createConstructor(cw, className, targetSignature, targetName); - createReadMethod(cw, className, source.getFields(), targetSignature, targetName); - createWriteMethod(cw, className, source.getFields(), targetSignature, targetName); - cw.visitEnd(); - - byte[] data = cw.toByteArray(); - - // Call the define method - try { - if (defineMethod == null) { - Method defined = ClassLoader.class.getDeclaredMethod("defineClass", - new Class[] { String.class, byte[].class, int.class, int.class }); - - // Awesome. Now, create and return it. - defined.setAccessible(true); - defineMethod = defined; - } - - @SuppressWarnings("rawtypes") - Class clazz = (Class) defineMethod.invoke(loader, null, data, 0, data.length); - - // DEBUG CODE: Print the content of the generated class. - //org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(data); - //cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0); - - return clazz; - - } catch (SecurityException e) { - throw new RuntimeException("Cannot use reflection to dynamically load a class.", e); - } catch (NoSuchMethodException e) { - throw new IllegalStateException("Incompatible JVM.", e); - } catch (IllegalArgumentException e) { - throw new IllegalStateException("Cannot call defineMethod - wrong JVM?", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Security limitation! Cannot dynamically load class.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Error occured in code generator.", e); - } - } - - /** - * Determine if at least one of the given fields is public. - * @param fields - field to test. - * @return TRUE if one or more field is publically accessible, FALSE otherwise. - */ - private boolean isAnyPublic(List fields) { - // Are any of the fields public? - for (int i = 0; i < fields.size(); i++) { - if (isPublic(fields.get(i))) { - return true; - } - } - - return false; - } - - private boolean isPublic(Field field) { - return Modifier.isPublic(field.getModifiers()); - } - - private boolean isNonFinal(Field field) { - return !Modifier.isFinal(field.getModifiers()); - } - - private void createFields(ClassWriter cw, String targetSignature) { - FieldVisitor typedField = cw.visitField(Opcodes.ACC_PRIVATE, "typedTarget", targetSignature, null, null); - typedField.visitEnd(); - } - - private void createWriteMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { - - String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; - String methodSignature = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; - MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "writeGenerated", methodDescriptor, methodSignature, - new String[] { FIELD_EXCEPTION_CLASS }); - BoxingHelper boxingHelper = new BoxingHelper(mv); - - String generatedClassName = PACKAGE_NAME + "/" + className; - - mv.visitCode(); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "typedTarget", targetSignature); - mv.visitVarInsn(Opcodes.ASTORE, 3); - mv.visitVarInsn(Opcodes.ILOAD, 1); - - // The last label is for the default switch - Label[] labels = new Label[fields.size()]; - Label errorLabel = new Label(); - Label returnLabel = new Label(); - - // Generate labels - for (int i = 0; i < fields.size(); i++) { - labels[i] = new Label(); - } - - mv.visitTableSwitchInsn(0, labels.length - 1, errorLabel, labels); - - for (int i = 0; i < fields.size(); i++) { - - Field field = fields.get(i); - Class outputType = field.getType(); - Class inputType = Primitives.wrap(outputType); - String typeDescriptor = Type.getDescriptor(outputType); - String inputPath = inputType.getName().replace('.', '/'); - - mv.visitLabel(labels[i]); - - // Push the compare object - if (i == 0) - mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); - else - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - - // Only write to public non-final fields - if (isPublic(field) && isNonFinal(field)) { - mv.visitVarInsn(Opcodes.ALOAD, 3); - mv.visitVarInsn(Opcodes.ALOAD, 2); - - if (!outputType.isPrimitive()) - mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); - else - boxingHelper.unbox(Type.getType(outputType)); - - mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, field.getName(), typeDescriptor); - - } else { - // Use reflection. We don't have a choice, unfortunately. - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ILOAD, 1); - mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, generatedClassName, "writeReflected", "(ILjava/lang/Object;)V"); - } - - mv.visitJumpInsn(Opcodes.GOTO, returnLabel); - } - - mv.visitLabel(errorLabel); - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); - mv.visitInsn(Opcodes.DUP); - mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); - mv.visitInsn(Opcodes.DUP); - mv.visitLdcInsn("Invalid index "); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); - mv.visitVarInsn(Opcodes.ILOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); - mv.visitInsn(Opcodes.ATHROW); - - mv.visitLabel(returnLabel); - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitInsn(Opcodes.ARETURN); - mv.visitMaxs(5, 4); - mv.visitEnd(); - } - - private void createReadMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { - MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "readGenerated", "(I)Ljava/lang/Object;", null, - new String[] { "com/comphenix/protocol/reflect/FieldAccessException" }); - BoxingHelper boxingHelper = new BoxingHelper(mv); - - String generatedClassName = PACKAGE_NAME + "/" + className; - - mv.visitCode(); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "typedTarget", targetSignature); - mv.visitVarInsn(Opcodes.ASTORE, 2); - mv.visitVarInsn(Opcodes.ILOAD, 1); - - // The last label is for the default switch - Label[] labels = new Label[fields.size()]; - Label errorLabel = new Label(); - - // Generate labels - for (int i = 0; i < fields.size(); i++) { - labels[i] = new Label(); - } - - mv.visitTableSwitchInsn(0, fields.size() - 1, errorLabel, labels); - - for (int i = 0; i < fields.size(); i++) { - - Field field = fields.get(i); - Class outputType = field.getType(); - String typeDescriptor = Type.getDescriptor(outputType); - - mv.visitLabel(labels[i]); - - // Push the compare object - if (i == 0) - mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); - else - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - - // Note that byte code cannot access non-public fields - if (isPublic(field)) { - mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitFieldInsn(Opcodes.GETFIELD, targetName, field.getName(), typeDescriptor); - - boxingHelper.box(Type.getType(outputType)); - } else { - // We have to use reflection for private and protected fields. - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ILOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, generatedClassName, "readReflected", "(I)Ljava/lang/Object;"); - } - - mv.visitInsn(Opcodes.ARETURN); - } - - mv.visitLabel(errorLabel); - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); - mv.visitInsn(Opcodes.DUP); - mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); - mv.visitInsn(Opcodes.DUP); - mv.visitLdcInsn("Invalid index "); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); - mv.visitVarInsn(Opcodes.ILOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); - mv.visitInsn(Opcodes.ATHROW); - mv.visitMaxs(5, 3); - mv.visitEnd(); - } - - private void createConstructor(ClassWriter cw, String className, String targetSignature, String targetName) { - MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", - "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", - "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", null); - String fullClassName = PACKAGE_NAME + "/" + className; - - mv.visitCode(); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "", "()V"); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ALOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, fullClassName, "initialize", "(L" + SUPER_CLASS + ";)V"); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ALOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, SUPER_CLASS, "getTarget", "()Ljava/lang/Object;"); - mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "target", "Ljava/lang/Object;"); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitFieldInsn(Opcodes.GETFIELD, fullClassName, "target", "Ljava/lang/Object;"); - mv.visitTypeInsn(Opcodes.CHECKCAST, targetName); - mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "typedTarget", targetSignature); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;"); - mv.visitInsn(Opcodes.RETURN); - mv.visitMaxs(2, 3); - mv.visitEnd(); - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.reflect.compiler; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.reflect.StructureModifier; +import com.google.common.base.Objects; +import com.google.common.primitives.Primitives; + +import net.sf.cglib.asm.*; + +// public class CompiledStructureModifierPacket20 extends CompiledStructureModifier { +// +// private Packet20NamedEntitySpawn typedTarget; +// +// public CompiledStructureModifierPacket20(StructureModifier other, StructureCompiler compiler) { +// super(); +// initialize(other); +// this.target = other.getTarget(); +// this.typedTarget = (Packet20NamedEntitySpawn) target; +// this.compiler = compiler; +// } +// +// @Override +// protected Object readGenerated(int fieldIndex) throws FieldAccessException { +// +// Packet20NamedEntitySpawn target = typedTarget; +// +// switch (fieldIndex) { +// case 0: return (Object) target.a; +// case 1: return (Object) target.b; +// case 2: return (Object) target.c; +// case 3: return super.readReflected(fieldIndex); +// case 4: return super.readReflected(fieldIndex); +// case 5: return (Object) target.f; +// case 6: return (Object) target.g; +// case 7: return (Object) target.h; +// default: +// throw new FieldAccessException("Invalid index " + fieldIndex); +// } +// } +// +// @Override +// protected StructureModifier writeGenerated(int index, Object value) throws FieldAccessException { +// +// Packet20NamedEntitySpawn target = typedTarget; +// +// switch (index) { +// case 0: target.a = (Integer) value; break; +// case 1: target.b = (String) value; break; +// case 2: target.c = (Integer) value; break; +// case 3: target.d = (Integer) value; break; +// case 4: super.writeReflected(index, value); break; +// case 5: super.writeReflected(index, value); break; +// case 6: target.g = (Byte) value; break; +// case 7: target.h = (Integer) value; break; +// default: +// throw new FieldAccessException("Invalid index " + index); +// } +// +// // Chaining +// return this; +// } +// } + +/** + * Represents a StructureModifier compiler. + * + * @author Kristian + */ +public final class StructureCompiler { + public static final ReportType REPORT_TOO_MANY_GENERATED_CLASSES = new ReportType("Generated too many classes (count: %s)"); + + // Used to store generated classes of different types + @SuppressWarnings("rawtypes") + static class StructureKey { + private Class targetType; + private Class fieldType; + + public StructureKey(StructureModifier source) { + this(source.getTargetType(), source.getFieldType()); + } + + public StructureKey(Class targetType, Class fieldType) { + this.targetType = targetType; + this.fieldType = fieldType; + } + + @Override + public int hashCode() { + return Objects.hashCode(targetType, fieldType); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StructureKey) { + StructureKey other = (StructureKey) obj; + return Objects.equal(targetType, other.targetType) && + Objects.equal(fieldType, other.fieldType); + } + return false; + } + } + + // Used to load classes + private volatile static Method defineMethod; + + @SuppressWarnings("rawtypes") + private Map compiledCache = new ConcurrentHashMap(); + + // The class loader we'll store our classes + private ClassLoader loader; + + // References to other classes + private static String PACKAGE_NAME = "com/comphenix/protocol/reflect/compiler"; + private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier"; + private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier"; + private static String FIELD_EXCEPTION_CLASS = "com/comphenix/protocol/reflect/FieldAccessException"; + + /** + * Construct a structure compiler. + * @param loader - main class loader. + */ + StructureCompiler(ClassLoader loader) { + this.loader = loader; + } + + /** + * Lookup the current class loader for any previously generated classes before we attempt to generate something. + * @param source - the structure modifier to look up. + * @return TRUE if we successfully found a previously generated class, FALSE otherwise. + */ + public boolean lookupClassLoader(StructureModifier source) { + StructureKey key = new StructureKey(source); + + // See if there's a need to lookup the class name + if (compiledCache.containsKey(key)) { + return true; + } + + try { + String className = getCompiledName(source); + + // This class might have been generated before. Try to load it. + Class before = loader.loadClass(PACKAGE_NAME.replace('/', '.') + "." + className); + + if (before != null) { + compiledCache.put(key, before); + return true; + } + } catch (ClassNotFoundException e) { + // That's ok. + } + + // We need to compile the class + return false; + } + + /** + * Compiles the given structure modifier. + *

+ * WARNING: Do NOT call this method in the main thread. Compiling may easily take 10 ms, which is already + * over 1/4 of a tick (50 ms). Let the background thread automatically compile the structure modifiers instead. + * @param source - structure modifier to compile. + * @return A compiled structure modifier. + */ + @SuppressWarnings("unchecked") + public synchronized StructureModifier compile(StructureModifier source) { + + // We cannot optimize a structure modifier with no public fields + if (!isAnyPublic(source.getFields())) { + return source; + } + + StructureKey key = new StructureKey(source); + Class compiledClass = compiledCache.get(key); + + if (!compiledCache.containsKey(key)) { + compiledClass = generateClass(source); + compiledCache.put(key, compiledClass); + } + + // Next, create an instance of this class + try { + return (StructureModifier) compiledClass.getConstructor( + StructureModifier.class, StructureCompiler.class). + newInstance(source, this); + } catch (OutOfMemoryError e) { + // Print the number of generated classes by the current instances + ProtocolLibrary.getErrorReporter().reportWarning( + this, Report.newBuilder(REPORT_TOO_MANY_GENERATED_CLASSES).messageParam(compiledCache.size()) + ); + throw e; + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Used invalid parameters in instance creation", e); + } catch (SecurityException e) { + throw new RuntimeException("Security limitation!", e); + } catch (InstantiationException e) { + throw new RuntimeException("Error occured while instancing generated class.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security limitation! Cannot create instance of dynamic class.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured while instancing generated class.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Cannot happen.", e); + } + } + + /** + * Retrieve a variable identifier that can uniquely represent the given type. + * @param type - a type. + * @return A unique and legal identifier for the given type. + */ + private String getSafeTypeName(Class type) { + return type.getCanonicalName().replace("[]", "Array").replace(".", "_"); + } + + /** + * Retrieve the compiled name of a given structure modifier. + * @param source - the structure modifier. + * @return The unique, compiled name of a compiled structure modifier. + */ + private String getCompiledName(StructureModifier source) { + Class targetType = source.getTargetType(); + + // Concat class and field type + return "CompiledStructure$" + + getSafeTypeName(targetType) + "$" + + getSafeTypeName(source.getFieldType()); + } + + /** + * Compile a structure modifier. + * @param source - structure modifier. + * @return The compiled structure modifier. + */ + private Class generateClass(StructureModifier source) { + + ClassWriter cw = new ClassWriter(0); + Class targetType = source.getTargetType(); + + String className = getCompiledName(source); + String targetSignature = Type.getDescriptor(targetType); + String targetName = targetType.getName().replace('.', '/'); + + // Define class + cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className, + null, COMPILED_CLASS, null); + + createFields(cw, targetSignature); + createConstructor(cw, className, targetSignature, targetName); + createReadMethod(cw, className, source.getFields(), targetSignature, targetName); + createWriteMethod(cw, className, source.getFields(), targetSignature, targetName); + cw.visitEnd(); + + byte[] data = cw.toByteArray(); + + // Call the define method + try { + if (defineMethod == null) { + Method defined = ClassLoader.class.getDeclaredMethod("defineClass", + new Class[] { String.class, byte[].class, int.class, int.class }); + + // Awesome. Now, create and return it. + defined.setAccessible(true); + defineMethod = defined; + } + + @SuppressWarnings("rawtypes") + Class clazz = (Class) defineMethod.invoke(loader, null, data, 0, data.length); + + // DEBUG CODE: Print the content of the generated class. + //org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(data); + //cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0); + + return clazz; + + } catch (SecurityException e) { + throw new RuntimeException("Cannot use reflection to dynamically load a class.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Incompatible JVM.", e); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Cannot call defineMethod - wrong JVM?", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security limitation! Cannot dynamically load class.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured in code generator.", e); + } + } + + /** + * Determine if at least one of the given fields is public. + * @param fields - field to test. + * @return TRUE if one or more field is publically accessible, FALSE otherwise. + */ + private boolean isAnyPublic(List fields) { + // Are any of the fields public? + for (int i = 0; i < fields.size(); i++) { + if (isPublic(fields.get(i))) { + return true; + } + } + + return false; + } + + private boolean isPublic(Field field) { + return Modifier.isPublic(field.getModifiers()); + } + + private boolean isNonFinal(Field field) { + return !Modifier.isFinal(field.getModifiers()); + } + + private void createFields(ClassWriter cw, String targetSignature) { + FieldVisitor typedField = cw.visitField(Opcodes.ACC_PRIVATE, "typedTarget", targetSignature, null, null); + typedField.visitEnd(); + } + + private void createWriteMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { + + String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + String methodSignature = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "writeGenerated", methodDescriptor, methodSignature, + new String[] { FIELD_EXCEPTION_CLASS }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + String generatedClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ASTORE, 3); + mv.visitVarInsn(Opcodes.ILOAD, 1); + + // The last label is for the default switch + Label[] labels = new Label[fields.size()]; + Label errorLabel = new Label(); + Label returnLabel = new Label(); + + // Generate labels + for (int i = 0; i < fields.size(); i++) { + labels[i] = new Label(); + } + + mv.visitTableSwitchInsn(0, labels.length - 1, errorLabel, labels); + + for (int i = 0; i < fields.size(); i++) { + + Field field = fields.get(i); + Class outputType = field.getType(); + Class inputType = Primitives.wrap(outputType); + String typeDescriptor = Type.getDescriptor(outputType); + String inputPath = inputType.getName().replace('.', '/'); + + mv.visitLabel(labels[i]); + + // Push the compare object + if (i == 0) + mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); + else + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + // Only write to public non-final fields + if (isPublic(field) && isNonFinal(field)) { + mv.visitVarInsn(Opcodes.ALOAD, 3); + mv.visitVarInsn(Opcodes.ALOAD, 2); + + if (!outputType.isPrimitive()) + mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); + else + boxingHelper.unbox(Type.getType(outputType)); + + mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, field.getName(), typeDescriptor); + + } else { + // Use reflection. We don't have a choice, unfortunately. + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, generatedClassName, "writeReflected", "(ILjava/lang/Object;)V"); + } + + mv.visitJumpInsn(Opcodes.GOTO, returnLabel); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Invalid index "); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); + mv.visitInsn(Opcodes.ATHROW); + + mv.visitLabel(returnLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(5, 4); + mv.visitEnd(); + } + + private void createReadMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "readGenerated", "(I)Ljava/lang/Object;", null, + new String[] { "com/comphenix/protocol/reflect/FieldAccessException" }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + String generatedClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ASTORE, 2); + mv.visitVarInsn(Opcodes.ILOAD, 1); + + // The last label is for the default switch + Label[] labels = new Label[fields.size()]; + Label errorLabel = new Label(); + + // Generate labels + for (int i = 0; i < fields.size(); i++) { + labels[i] = new Label(); + } + + mv.visitTableSwitchInsn(0, fields.size() - 1, errorLabel, labels); + + for (int i = 0; i < fields.size(); i++) { + + Field field = fields.get(i); + Class outputType = field.getType(); + String typeDescriptor = Type.getDescriptor(outputType); + + mv.visitLabel(labels[i]); + + // Push the compare object + if (i == 0) + mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); + else + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + // Note that byte code cannot access non-public fields + if (isPublic(field)) { + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.GETFIELD, targetName, field.getName(), typeDescriptor); + + boxingHelper.box(Type.getType(outputType)); + } else { + // We have to use reflection for private and protected fields. + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, generatedClassName, "readReflected", "(I)Ljava/lang/Object;"); + } + + mv.visitInsn(Opcodes.ARETURN); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Invalid index "); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); + mv.visitInsn(Opcodes.ATHROW); + mv.visitMaxs(5, 3); + mv.visitEnd(); + } + + private void createConstructor(ClassWriter cw, String className, String targetSignature, String targetName) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", + "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", + "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", null); + String fullClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "", "()V"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, fullClassName, "initialize", "(L" + SUPER_CLASS + ";)V"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, SUPER_CLASS, "getTarget", "()Ljava/lang/Object;"); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "target", "Ljava/lang/Object;"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, fullClassName, "target", "Ljava/lang/Object;"); + mv.visitTypeInsn(Opcodes.CHECKCAST, targetName); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;"); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(2, 3); + mv.visitEnd(); + } +}