diff --git a/ItemDisguise/.classpath b/ItemDisguise/.classpath index 71e70473..2bda6dc7 100644 --- a/ItemDisguise/.classpath +++ b/ItemDisguise/.classpath @@ -7,6 +7,12 @@ + + + + + + diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index ac7a8b99..414e2599 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.3.0 + 2.4.3 jar Provides read/write access to the Minecraft protocol. @@ -57,7 +57,7 @@ false - true + false @@ -203,7 +203,7 @@ org.bukkit craftbukkit - 1.4.7-R0.1 + 1.5.1-R0.2-SNAPSHOT provided @@ -219,16 +219,16 @@ test - org.powermock - powermock-module-junit4 - ${powermock.version} - test - - - org.powermock - powermock-api-mockito - ${powermock.version} - test - + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + \ No newline at end of file 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 18ddb25e..7ca949bd 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java @@ -1,109 +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)) { - 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 { - 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 new file mode 100644 index 00000000..392b1efc --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -0,0 +1,482 @@ +package com.comphenix.protocol; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.conversations.Conversable; +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationAbandonedEvent; +import org.bukkit.conversations.ConversationAbandonedListener; +import org.bukkit.conversations.ConversationContext; +import org.bukkit.conversations.ConversationFactory; +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; +import com.google.common.collect.Ranges; + +/** + * A command to apply JavaScript filtering to the packet command. + * + * @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. + * @param event - the packet event. + * @param filter - the filter that failed. + * @param ex - the failure. + * @returns TRUE to keep processing this filter, FALSE to remove it. + */ + public boolean handle(PacketEvent event, Filter filter, Exception ex); + } + + /** + * Possible sub commands. + * + * @author Kristian + */ + private enum SubCommand { + ADD, REMOVE; + } + + /** + * A filter that will be used to process a packet event. + * @author Kristian + */ + public static class Filter { + private final String name; + private final String predicate; + + private final IntegerSet ranges; + + /** + * Construct a new immutable filter. + * @param name - the unique name of the filter. + * @param predicate - the JavaScript predicate that will be used to filter packet events. + * @param ranges - a list of valid packet ID ranges that this filter applies to. + */ + public Filter(String name, String predicate, Set packets) { + this.name = name; + this.predicate = predicate; + this.ranges = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + this.ranges.addAll(packets); + } + + /** + * Retrieve the unique name of the filter. + * @return Unique name of the filter. + */ + public String getName() { + return name; + } + + /** + * Retrieve the JavaScript predicate that will be used to filter packet events. + * @return Predicate itself. + */ + public String getPredicate() { + return predicate; + } + + /** + * Retrieve a copy of the set of packets this filter applies to. + * @return Set of packets this filter applies to. + */ + public Set getRanges() { + return ranges.toSet(); + } + + /** + * Determine whether or not a packet event needs to be passed to this filter. + * @param event - the event to test. + * @return TRUE if it does, FALSE otherwise. + */ + private boolean isApplicable(PacketEvent event) { + return ranges.contains(event.getPacketID()); + } + + /** + * Evaluate the current filter using the provided ScriptEngine as context. + *

+ * This context may be modified with additional code. + * @param context - the current script context. + * @param event - the packet event to evaluate. + * @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise. + * @throws ScriptException If the compilation failed or the filter is not valid. + */ + public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException { + if (!isApplicable(event)) + return true; + // Ensure that the predicate has been compiled + compile(context); + + try { + Object result = ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle()); + + if (result instanceof Boolean) + return (Boolean) result; + else + throw new ScriptException("Filter result wasn't a boolean: " + result); + + } catch (NoSuchMethodException e) { + // Must be a fault with the script engine itself + throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e); + } + } + + /** + * Force the compilation of a specific filter. + * @param context - the current script context. + * @throws ScriptException If the compilation failed. + */ + public void compile(ScriptEngine context) throws ScriptException { + if (context.get(name) == null) { + context.eval("var " + name + " = function(event, packet) {\n" + predicate); + } + } + + /** + * Clean up all associated code from this filter in the provided script engine. + * @param context - the current script context. + */ + public void close(ScriptEngine context) { + context.put(name, null); + } + } + + private class CompilationSuccessCanceller implements MultipleConversationCanceller { + @Override + public boolean cancelBasedOnInput(ConversationContext context, String in) { + throw new UnsupportedOperationException("Cannot cancel on the last line alone."); + } + + @Override + public void setConversation(Conversation conversation) { + // Ignore + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine, StringBuilder lines, int lineCount) { + try { + engine.eval("function(event, packet) {\n" + lines.toString()); + + // It compiles - accept the filter! + return true; + } catch (ScriptException e) { + // We also have the function() line + int realLineCount = lineCount + 1; + + // Only possible to recover from an error on the last line. + return e.getLineNumber() < realLineCount; + } + } + + @Override + public CompilationSuccessCanceller clone() { + return new CompilationSuccessCanceller(); + } + } + + /** + * Name of this command. + */ + public static final String NAME = "filter"; + + // Default error handler + private FilterFailedHandler defaultFailedHandler; + + // Currently registered filters + private List filters = new ArrayList(); + + // Owner plugin + private final Plugin plugin; + + // Whether or not the command is enabled + private ProtocolConfig config; + + // Script engine + private ScriptEngine engine; + + public CommandFilter(ErrorReporter reporter, Plugin plugin, ProtocolConfig config) { + super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 2); + this.plugin = plugin; + this.config = config; + + // Start the engine + initalizeScript(); + } + + private void initalizeScript() { + try { + // First attempt + initializeEngine(); + + // Oh for .. + if (!isInitialized()) { + throw new ScriptException("A JavaScript engine could not be found."); + } + } catch (ScriptException e1) { + // It's not a huge deal + printPackageWarning(e1); + + if (!config.getScriptEngineName().equals("rhino")) { + reporter.reportWarning(this, Report.newBuilder(REPORT_FALLBACK_ENGINE)); + config.setScriptEngineName("rhino"); + config.saveAll(); + + try { + initializeEngine(); + + if (!isInitialized()) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_FALLBACK_ENGINE)); + } + } catch (ScriptException e2) { + // And again .. + printPackageWarning(e2); + } + } + } + } + + private void printPackageWarning(ScriptException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE).error(e)); + } + + /** + * Initialize the current configured engine. + * @throws ScriptException If we are unable to import packages. + */ + private void initializeEngine() throws ScriptException { + ScriptEngineManager manager = new ScriptEngineManager(); + engine = manager.getEngineByName(config.getScriptEngineName()); + + // Import useful packages + if (engine != null) { + engine.eval("importPackage(org.bukkit);"); + engine.eval("importPackage(com.comphenix.protocol.reflect);"); + } + } + + /** + * Determine if the filter engine has been successfully initialized. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean isInitialized() { + return engine != null; + } + + private FilterFailedHandler getDefaultErrorHandler() { + // No need to create a new object every time + if (defaultFailedHandler == null) { + defaultFailedHandler = new FilterFailedHandler() { + @Override + public boolean handle(PacketEvent event, Filter filter, Exception ex) { + reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event); + reporter.reportWarning(this, + Report.newBuilder(REPORT_FILTER_REMOVED_FOR_ERROR).messageParam(filter.getName(), ex.getClass().getSimpleName()) + ); + return false; + } + }; + } + return defaultFailedHandler; + } + + /** + * Determine whether or not to pass the given packet event to the packet listeners. + *

+ * Uses a default filter failure handler that simply prints the error message and removes the filter. + * @param event - the event. + * @return TRUE if we should, FALSE otherwise. + */ + public boolean filterEvent(PacketEvent event) { + return filterEvent(event, getDefaultErrorHandler()); + } + + /** + * Determine whether or not to pass the given packet event to the packet listeners. + * @param event - the event. + * @param handler - failure handler. + * @return TRUE if we should, FALSE otherwise. + * @throws FilterFailedException If one of the filters failed. + */ + public boolean filterEvent(PacketEvent event, FilterFailedHandler handler) { + for (Iterator it = filters.iterator(); it.hasNext(); ) { + Filter filter = it.next(); + + try { + if (!filter.evaluate(engine, event)) { + return false; + } + } catch (Exception ex) { + if (!handler.handle(event, filter, ex)) { + it.remove(); + } + } + } + // Pass! + return true; + } + + /* + * Description: Adds or removes a simple packet filter. + Usage: / add|remove name [packet IDs] + */ + @Override + protected boolean handleCommand(CommandSender sender, String[] args) { + if (!config.isDebug()) { + sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!"); + return true; + } + if (!isInitialized()) { + sender.sendMessage(ChatColor.RED + "JavaScript engine was not present. Filter system is disabled."); + return true; + } + + final SubCommand command = parseCommand(args, 0); + final String name = args[1]; + + switch (command) { + case ADD: + // Never overwrite an existing filter + if (findFilter(name) != null) { + sender.sendMessage(ChatColor.RED + "Filter " + name + " already exists. Remove it first."); + return true; + } + + final Set packets = parseRanges(args, 2); + sender.sendMessage("Enter filter program ('}' to complete or CANCEL):"); + + // Make sure we can use the conversable interface + if (sender instanceof Conversable) { + final MultipleLinesPrompt prompt = + new MultipleLinesPrompt(new CompilationSuccessCanceller(), "function(event, packet) {"); + + new ConversationFactory(plugin). + withFirstPrompt(prompt). + withEscapeSequence("CANCEL"). + withLocalEcho(false). + addConversationAbandonedListener(new ConversationAbandonedListener() { + @Override + public void conversationAbandoned(ConversationAbandonedEvent event) { + try { + final Conversable whom = event.getContext().getForWhom(); + + if (event.gracefulExit()) { + String predicate = prompt.removeAccumulatedInput(event.getContext()); + Filter filter = new Filter(name, predicate, packets); + + // Print the last line as well + whom.sendRawMessage(prompt.getPromptText(event.getContext())); + + try { + // Force early compilation + filter.compile(engine); + + filters.add(filter); + whom.sendRawMessage(ChatColor.GOLD + "Added filter " + name); + } catch (ScriptException e) { + e.printStackTrace(); + whom.sendRawMessage(ChatColor.GOLD + "Compilation error: " + e.getMessage()); + } + } else { + // Too bad + whom.sendRawMessage(ChatColor.RED + "Cancelled filter."); + } + } catch (Exception e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_HANDLE_CONVERSATION).error(e).callerParam(event) + ); + } + } + }). + buildConversation((Conversable) sender). + begin(); + } else { + sender.sendMessage(ChatColor.RED + "Only console and players are supported!"); + } + + break; + + case REMOVE: + Filter filter = findFilter(name); + + // See if it exists before we remove it + if (filter != null) { + filter.close(engine); + filters.remove(filter); + sender.sendMessage(ChatColor.GOLD + "Removed filter " + name); + } else { + sender.sendMessage(ChatColor.RED + "Unable to find a filter by the name " + name); + } + break; + } + + return true; + } + + private Set parseRanges(String[] args, int start) { + List> ranges = RangeParser.getRanges(args, 2, args.length - 1, Ranges.closed(0, 255)); + Set flatten = new HashSet(); + + if (ranges.isEmpty()) { + // Use every packet ID + ranges.add(Ranges.closed(0, 255)); + } + + // Finally, flatten it all + for (Range range : ranges) { + flatten.addAll(range.asSet(DiscreteDomains.integers())); + } + return flatten; + } + + /** + * Lookup a filter by its name. + * @param name - the filter name. + * @return The filter, or NULL if not found. + */ + private Filter findFilter(String name) { + // We'll just use a linear scan for now - we don't expect that many filters + for (Filter filter : filters) { + if (filter.getName().equalsIgnoreCase(name)) { + return filter; + } + } + return null; + } + + private SubCommand parseCommand(String[] args, int index) { + String text = args[index].toUpperCase(); + + try { + return SubCommand.valueOf(text); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.", e); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index 6f098599..72eac4c4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -1,533 +1,544 @@ -/* - * 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); - - public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, ProtocolManager manager) { - super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); - this.plugin = plugin; - this.logger = logger; - this.manager = manager; - 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()); - 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()) { - printInformation(event); - } - } - - @Override - public void onPacketReceiving(PacketEvent event) { - if (side.isForClient()) { - 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()); + + 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/MultipleLinesPrompt.java b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java new file mode 100644 index 00000000..53f1229e --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java @@ -0,0 +1,163 @@ +package com.comphenix.protocol; + +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationCanceller; +import org.bukkit.conversations.ConversationContext; +import org.bukkit.conversations.ExactMatchConversationCanceller; +import org.bukkit.conversations.Prompt; +import org.bukkit.conversations.StringPrompt; + +/** + * Represents a conversation prompt that accepts a list of lines. + * + * @author Kristian + */ +class MultipleLinesPrompt extends StringPrompt { + /** + * Represents a canceller that determines if the multiple lines prompt is finished. + * @author Kristian + */ + public static interface MultipleConversationCanceller extends ConversationCanceller { + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine); + + /** + * Determine if the current prompt is done based on the context, last + * line and collected lines. + * + * @param context - current context. + * @param currentLine - current (last) line. + * @param lines - collected lines. + * @param lineCount - number of lines. + * @return TRUE if we are done, FALSE otherwise. + */ + public boolean cancelBasedOnInput(ConversationContext context, String currentLine, + StringBuilder lines, int lineCount); + } + + /** + * A wrapper class for turning a ConversationCanceller into a MultipleConversationCanceller. + * @author Kristian + */ + private static class MultipleWrapper implements MultipleConversationCanceller { + private ConversationCanceller canceller; + + public MultipleWrapper(ConversationCanceller canceller) { + this.canceller = canceller; + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine) { + return canceller.cancelBasedOnInput(context, currentLine); + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine, + StringBuilder lines, int lineCount) { + return cancelBasedOnInput(context, currentLine); + } + + @Override + public void setConversation(Conversation conversation) { + canceller.setConversation(conversation); + } + + @Override + public MultipleWrapper clone() { + return new MultipleWrapper(canceller.clone()); + } + } + + // Feels a bit like Android + private static final String KEY = "multiple_lines_prompt"; + private static final String KEY_LAST = KEY + ".last_line"; + private static final String KEY_LINES = KEY + ".linecount"; + + private final MultipleConversationCanceller endMarker; + private final String initialPrompt; + + /** + * Retrieve and remove the current accumulated input. + * + * @param context + * - conversation context. + * @return The accumulated input, or NULL if not found. + */ + public String removeAccumulatedInput(ConversationContext context) { + Object result = context.getSessionData(KEY); + + if (result instanceof StringBuilder) { + context.setSessionData(KEY, null); + context.setSessionData(KEY_LINES, null); + return ((StringBuilder) result).toString(); + } else { + return null; + } + } + + /** + * Construct a multiple lines input prompt with a specific end marker. + *

+ * This is usually an empty string. + * + * @param endMarker - the end marker. + */ + public MultipleLinesPrompt(String endMarker, String initialPrompt) { + this(new ExactMatchConversationCanceller(endMarker), initialPrompt); + } + + /** + * Construct a multiple lines input prompt with a specific end marker implementation. + *

+ * Note: Use {@link #MultipleLinesPrompt(MultipleConversationCanceller, String)} if implementing a custom canceller. + * @param endMarker - the end marker. + * @param initialPrompt - the initial prompt text. + */ + public MultipleLinesPrompt(ConversationCanceller endMarker, String initialPrompt) { + this.endMarker = new MultipleWrapper(endMarker); + this.initialPrompt = initialPrompt; + } + + /** + * Construct a multiple lines input prompt with a specific end marker implementation. + * @param endMarker - the end marker. + * @param initialPrompt - the initial prompt text. + */ + public MultipleLinesPrompt(MultipleConversationCanceller endMarker, String initialPrompt) { + this.endMarker = endMarker; + this.initialPrompt = initialPrompt; + } + + @Override + public Prompt acceptInput(ConversationContext context, String in) { + StringBuilder result = (StringBuilder) context.getSessionData(KEY); + Integer count = (Integer) context.getSessionData(KEY_LINES); + + // Handle first run + if (result == null) + context.setSessionData(KEY, result = new StringBuilder()); + if (count == null) + count = 0; + + // Save the last line as well + context.setSessionData(KEY_LAST, in); + context.setSessionData(KEY_LINES, ++count); + result.append(in + "\n"); + + // And we're done + if (endMarker.cancelBasedOnInput(context, in, result, count)) + return Prompt.END_OF_CONVERSATION; + else + return this; + } + + @Override + public String getPromptText(ConversationContext context) { + Object last = context.getSessionData(KEY_LAST); + + if (last instanceof String) + return (String) last; + else + return initialPrompt; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java b/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java index 6d07232c..a243452a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java @@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException; import org.bukkit.entity.Player; +import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketContainer; /** @@ -43,7 +44,7 @@ public interface PacketStream { * Send a packet to the given player. * @param reciever - the reciever. * @param packet - packet to send. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException - if an error occured when sending the packet. */ public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) @@ -63,7 +64,7 @@ public interface PacketStream { * Simulate recieving a certain packet from a given player. * @param sender - the sender. * @param packet - the packet that was sent. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException If the reflection machinery failed. * @throws IllegalAccessException If the underlying method caused an error. */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java index ada35964..8b543e32 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java @@ -114,6 +114,10 @@ public final class Packets { public static final int PLAYER_INFO = 201; public static final int ABILITIES = 202; public static final int TAB_COMPLETE = 203; + public static final int SCOREBOARD_OBJECTIVE = 206; + public static final int UPDATE_SCORE = 207; + public static final int DISPLAY_SCOREBOARD = 208; + public static final int TEAMS = 209; public static final int CUSTOM_PAYLOAD = 250; public static final int KEY_RESPONSE = 252; public static final int KEY_REQUEST = 253; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index 87e5b523..269bcb04 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -18,12 +18,15 @@ package com.comphenix.protocol; import java.io.File; +import java.io.IOException; import org.bukkit.configuration.Configuration; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.google.common.base.Charsets; +import com.google.common.io.Files; /** * Represents the configuration of ProtocolLib. @@ -31,6 +34,7 @@ import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; * @author Kristian */ class ProtocolConfig { + private static final String LAST_UPDATE_FILE = "lastupdate"; private static final String SECTION_GLOBAL = "global"; private static final String SECTION_AUTOUPDATER = "auto updater"; @@ -40,12 +44,14 @@ class ProtocolConfig { private static final String IGNORE_VERSION_CHECK = "ignore version check"; private static final String BACKGROUND_COMPILER_ENABLED = "background compiler"; + private static final String DEBUG_MODE_ENABLED = "debug"; private static final String INJECTION_METHOD = "injection method"; + private static final String SCRIPT_ENGINE_NAME = "script engine"; + private static final String UPDATER_NOTIFY = "notify"; private static final String UPDATER_DOWNLAD = "download"; private static final String UPDATER_DELAY = "delay"; - private static final String UPDATER_LAST_TIME = "last"; // Defaults private static final long DEFAULT_UPDATER_DELAY = 43200; @@ -57,6 +63,11 @@ class ProtocolConfig { private ConfigurationSection global; private ConfigurationSection updater; + // Last update time + private long lastUpdateTime; + private boolean configChanged; + private boolean valuesChanged; + public ProtocolConfig(Plugin plugin) { this(plugin, plugin.getConfig()); } @@ -70,10 +81,64 @@ class ProtocolConfig { * Reload configuration file. */ public void reloadConfig() { + // Reset + configChanged = false; + valuesChanged = false; + this.config = plugin.getConfig(); + this.lastUpdateTime = loadLastUpdate(); loadSections(!loadingSections); } + /** + * Load the last update time stamp from the file system. + * @return Last update time stamp. + */ + private long loadLastUpdate() { + File dataFile = getLastUpdateFile(); + + if (dataFile.exists()) { + try { + return Long.parseLong(Files.toString(dataFile, Charsets.UTF_8)); + } catch (NumberFormatException e) { + throw new RuntimeException("Cannot parse " + dataFile + " as a number.", e); + } catch (IOException e) { + throw new RuntimeException("Cannot read " + dataFile, e); + } + } else { + // Default last update + return 0; + } + } + + /** + * Store the given time stamp. + * @param value - time stamp to store. + */ + private void saveLastUpdate(long value) { + File dataFile = getLastUpdateFile(); + + // The data folder must exist + dataFile.getParentFile().mkdirs(); + + if (dataFile.exists()) + dataFile.delete(); + + try { + Files.write(Long.toString(value), dataFile, Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Cannot write " + dataFile, e); + } + } + + /** + * Retrieve the file that is used to store the update time stamp. + * @return File storing the update time stamp. + */ + private File getLastUpdateFile() { + return new File(plugin.getDataFolder(), LAST_UPDATE_FILE); + } + /** * Load data sections. * @param copyDefaults - whether or not to copy configuration defaults. @@ -100,6 +165,17 @@ class ProtocolConfig { System.out.println("[ProtocolLib] Created default configuration."); } } + + /** + * Set a particular configuration key value pair. + * @param section - the configuration root. + * @param path - the path to the key. + * @param value - the value to set. + */ + private void setConfig(ConfigurationSection section, String path, Object value) { + configChanged = true; + section.set(path, value); + } /** * Retrieve a reference to the configuration file. @@ -122,7 +198,7 @@ class ProtocolConfig { * @param value - TRUE to do this automatically, FALSE otherwise. */ public void setAutoNotify(boolean value) { - updater.set(UPDATER_NOTIFY, value); + setConfig(updater, UPDATER_NOTIFY, value); } /** @@ -138,7 +214,25 @@ class ProtocolConfig { * @param value - TRUE if it should. FALSE otherwise. */ public void setAutoDownload(boolean value) { - updater.set(UPDATER_DOWNLAD, value); + setConfig(updater, UPDATER_DOWNLAD, value); + } + + /** + * Determine whether or not debug mode is enabled. + *

+ * This grants access to the filter command. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean isDebug() { + return global.getBoolean(DEBUG_MODE_ENABLED, false); + } + + /** + * Set whether or not debug mode is enabled. + * @param value - TRUE if it is enabled, FALSE otherwise. + */ + public void setDebug(boolean value) { + setConfig(global, DEBUG_MODE_ENABLED, value); } /** @@ -160,17 +254,9 @@ class ProtocolConfig { // Silently fix the delay if (delaySeconds < DEFAULT_UPDATER_DELAY) delaySeconds = DEFAULT_UPDATER_DELAY; - updater.set(UPDATER_DELAY, delaySeconds); + setConfig(updater, UPDATER_DELAY, delaySeconds); } - /** - * Retrieve the last time we updated, in seconds since 1970.01.01 00:00. - * @return Last update time. - */ - public long getAutoLastTime() { - return updater.getLong(UPDATER_LAST_TIME, 0); - } - /** * The version of Minecraft to ignore the built-in safety feature. * @return The version to ignore ProtocolLib's satefy. @@ -188,7 +274,7 @@ class ProtocolConfig { * @param ignoreVersion - the version of Minecraft where the satefy will be disabled. */ public void setIgnoreVersionCheck(String ignoreVersion) { - global.set(IGNORE_VERSION_CHECK, ignoreVersion); + setConfig(global, IGNORE_VERSION_CHECK, ignoreVersion); } /** @@ -207,7 +293,7 @@ class ProtocolConfig { * @param enabled - whether or not metrics is enabled. */ public void setMetricsEnabled(boolean enabled) { - global.set(METRICS_ENABLED, enabled); + setConfig(global, METRICS_ENABLED, enabled); } /** @@ -226,7 +312,15 @@ class ProtocolConfig { * @param enabled - TRUE if is enabled/running, FALSE otherwise. */ public void setBackgroundCompilerEnabled(boolean enabled) { - global.set(BACKGROUND_COMPILER_ENABLED, enabled); + setConfig(global, BACKGROUND_COMPILER_ENABLED, enabled); + } + + /** + * Retrieve the last time we updated, in seconds since 1970.01.01 00:00. + * @return Last update time. + */ + public long getAutoLastTime() { + return lastUpdateTime; } /** @@ -234,7 +328,26 @@ class ProtocolConfig { * @param lastTimeSeconds - new last update time. */ public void setAutoLastTime(long lastTimeSeconds) { - updater.set(UPDATER_LAST_TIME, lastTimeSeconds); + this.valuesChanged = true; + this.lastUpdateTime = lastTimeSeconds; + } + + /** + * Retrieve the unique name of the script engine to use for filtering. + * @return Script engine to use. + */ + public String getScriptEngineName() { + return global.getString(SCRIPT_ENGINE_NAME, "JavaScript"); + } + + /** + * Set the unique name of the script engine to use for filtering. + *

+ * This setting will take effect next time ProtocolLib is started. + * @param name - name of the script engine to use. + */ + public void setScriptEngineName(String name) { + setConfig(global, SCRIPT_ENGINE_NAME, name); } /** @@ -266,13 +379,20 @@ class ProtocolConfig { * @return Injection method. */ public void setInjectionMethod(PlayerInjectHooks hook) { - global.set(INJECTION_METHOD, hook.name()); + setConfig(global, INJECTION_METHOD, hook.name()); } /** * Save the current configuration file. */ public void saveAll() { - plugin.saveConfig(); + if (valuesChanged) + saveLastUpdate(lastUpdateTime); + if (configChanged) + plugin.saveConfig(); + + // And we're done + valuesChanged = false; + configChanged = false; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 271c1181..b93c662a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -33,8 +33,11 @@ import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.async.AsyncFilterManager; +import com.comphenix.protocol.error.BasicErrorReporter; 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; @@ -43,6 +46,7 @@ import com.comphenix.protocol.metrics.Updater; import com.comphenix.protocol.metrics.Updater.UpdateResult; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; import com.comphenix.protocol.utility.ChatExtensions; +import com.comphenix.protocol.utility.MinecraftVersion; /** * The main entry point for ProtocolLib. @@ -50,6 +54,24 @@ import com.comphenix.protocol.utility.ChatExtensions; * @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. */ @@ -58,7 +80,7 @@ public class ProtocolLibrary extends JavaPlugin { /** * The maximum version ProtocolLib has been tested with, */ - private static final String MAXIMUM_MINECRAFT_VERSION = "1.4.7"; + private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.2"; /** * The number of milliseconds per second. @@ -71,7 +93,7 @@ public class ProtocolLibrary extends JavaPlugin { private static PacketFilterManager protocolManager; // Error reporter - private static ErrorReporter reporter; + private static ErrorReporter reporter = new BasicErrorReporter(); // Metrics and statistisc private Statistics statistisc; @@ -102,6 +124,7 @@ public class ProtocolLibrary extends JavaPlugin { // Commands private CommandProtocol commandProtocol; private CommandPacket commandPacket; + private CommandFilter commandFilter; // Whether or not disable is not needed private boolean skipDisable; @@ -118,26 +141,34 @@ 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)); } } + // Print the state of the debug mode + if (config.isDebug()) { + logger.warning("Debug mode is enabled!"); + } + try { // Check for other versions checkConflictingVersions(); + // Handle unexpected Minecraft versions + MinecraftVersion version = verifyMinecraftVersion(); + // Set updater updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info"); unhookTask = new DelayedSingleTask(this); protocolManager = new PacketFilterManager( - getClassLoader(), getServer(), unhookTask, detailedReporter); + getClassLoader(), getServer(), this, version, unhookTask, detailedReporter); // Setup error reporter detailedReporter.addGlobalParameter("manager", protocolManager); @@ -152,18 +183,19 @@ 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 commandProtocol = new CommandProtocol(detailedReporter, this, updater, config); - commandPacket = new CommandPacket(detailedReporter, this, logger, protocolManager); + commandFilter = new CommandFilter(detailedReporter, this, config); + commandPacket = new CommandPacket(detailedReporter, this, logger, commandFilter, protocolManager); // Send logging information to player listeners too 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(); } } @@ -249,12 +281,10 @@ public class ProtocolLibrary extends JavaPlugin { logger.info("Structure compiler thread has been disabled."); } - // Handle unexpected Minecraft versions - verifyMinecraftVersion(); - // Set up command handlers registerCommand(CommandProtocol.NAME, commandProtocol); registerCommand(CommandPacket.NAME, commandPacket); + registerCommand(CommandFilter.NAME, commandFilter); // Player login and logout events protocolManager.registerEvents(manager, this); @@ -264,7 +294,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; } @@ -275,14 +305,14 @@ 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)); } } // Used to check Minecraft version - private void verifyMinecraftVersion() { + private MinecraftVersion verifyMinecraftVersion() { try { MinecraftVersion minimum = new MinecraftVersion(MINIMUM_MINECRAFT_VERSION); MinecraftVersion maximum = new MinecraftVersion(MAXIMUM_MINECRAFT_VERSION); @@ -296,9 +326,14 @@ public class ProtocolLibrary extends JavaPlugin { if (current.compareTo(maximum) > 0) logger.warning("Version " + current + " has not yet been tested! Proceed with caution."); } + 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 + return null; } private void checkConflictingVersions() { @@ -331,7 +366,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 @@ -360,7 +395,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) + ); } } @@ -394,7 +431,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)); } } } @@ -417,7 +454,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; } } @@ -454,7 +491,9 @@ public class ProtocolLibrary extends JavaPlugin { unhookTask.close(); protocolManager = null; statistisc = null; - reporter = null; + + // To clean up global parameters + reporter = new BasicErrorReporter(); // Leaky ClassLoader begone! if (updater == null || updater.getResult() != UpdateResult.SUCCESS) { @@ -479,6 +518,8 @@ public class ProtocolLibrary extends JavaPlugin { /** * Retrieve the current error reporter. + *

+ * This is guaranteed to not be NULL in 2.5.0 and later. * @return Current error reporter. */ public static ErrorReporter getErrorReporter() { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java index 741c54bc..a9da948f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java @@ -27,6 +27,7 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.async.AsyncMarker; +import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.PacketConstructor; @@ -47,7 +48,7 @@ public interface ProtocolManager extends PacketStream { * * @param reciever - the reciever. * @param packet - packet to send. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException - if an error occured when sending the packet. */ @Override @@ -62,7 +63,7 @@ public interface ProtocolManager extends PacketStream { * * @param sender - the sender. * @param packet - the packet that was sent. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException If the reflection machinery failed. * @throws IllegalAccessException If the underlying method caused an error. */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java index 800124f2..3671ba0e 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java @@ -23,6 +23,7 @@ import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.Semaphore; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.PrioritizedListener; @@ -66,7 +67,7 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap sendingQueue; - - // Asynchronous packet sending - private Executor asynchronousSender; - - // Whether or not packet transmission must occur on a specific thread - private final boolean notThreadSafe; - - // Whether or not we've run the cleanup procedure - private boolean cleanedUp = false; - - /** - * Create a packet sending queue. - * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. - */ - public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { - this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY); - this.notThreadSafe = notThreadSafe; - this.asynchronousSender = asynchronousSender; - } - - /** - * Number of packet events in the queue. - * @return The number of packet events in the queue. - */ - public int size() { - return sendingQueue.size(); - } - - /** - * Enqueue a packet for sending. - * @param packet - packet to queue. - */ - public void enqueue(PacketEvent packet) { - sendingQueue.add(new PacketEventHolder(packet)); - } - - /** - * Invoked when one of the packets have finished processing. - * @param packetUpdated - the packet that has now been updated. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { - - AsyncMarker marker = packetUpdated.getAsyncMarker(); - - // Should we reorder the event? - if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { - PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); - - // "Cancel" the original event - packetUpdated.setCancelled(true); - - // Enqueue the copy with the new sending index - enqueue(copy); - } - - // Mark this packet as finished - marker.setProcessed(true); - trySendPackets(onMainThread); - } - - /*** - * Invoked when a list of packet IDs are no longer associated with any listeners. - * @param packetsRemoved - packets that no longer have any listeners. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { - - Set lookup = new HashSet(packetsRemoved); - - // Note that this is O(n), so it might be expensive - for (PacketEventHolder holder : sendingQueue) { - PacketEvent event = holder.getEvent(); - - if (lookup.contains(event.getPacketID())) { - event.getAsyncMarker().setProcessed(true); - } - } - - // This is likely to have changed the situation a bit - trySendPackets(onMainThread); - } - - /** - * Attempt to send any remaining packets. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public void trySendPackets(boolean onMainThread) { - // Whether or not to continue sending packets - boolean sending = true; - - // Transmit as many packets as we can - while (sending) { - PacketEventHolder holder = sendingQueue.poll(); - - if (holder != null) { - sending = processPacketHolder(onMainThread, holder); - - if (!sending) { - // Add it back again - sendingQueue.add(holder); - } - - } else { - // No more packets to send - sending = false; - } - } - } - - /** - * Invoked when a packet might be ready for transmission. - * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. - * @param holder - packet container. - * @return TRUE to continue sending packets, FALSE otherwise. - */ - private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { - PacketEvent current = holder.getEvent(); - AsyncMarker marker = current.getAsyncMarker(); - boolean hasExpired = marker.hasExpired(); - - // Guard in cause the queue is closed - if (cleanedUp) { - return true; - } - - // End condition? - if (marker.isProcessed() || hasExpired) { - if (hasExpired) { - // Notify timeout listeners - onPacketTimeout(current); - - // Recompute - marker = current.getAsyncMarker(); - hasExpired = marker.hasExpired(); - - // Could happen due to the timeout listeners - if (!marker.isProcessed() && !hasExpired) { - return false; - } - } - - // Is it okay to send the packet? - if (!current.isCancelled() && !hasExpired) { - // Make sure we're on the main thread - if (notThreadSafe) { - try { - boolean wantAsync = marker.isMinecraftAsync(current); - boolean wantSync = !wantAsync; - - // Wait for the next main thread heartbeat if we haven't fulfilled our promise - if (!onMainThread && wantSync) { - return false; - } - - // Let's give it what it wants - if (onMainThread && wantAsync) { - asynchronousSender.execute(new Runnable() { - @Override - public void run() { - // We know this isn't on the main thread - processPacketHolder(false, holder); - } - }); - - // Scheduler will do the rest - return true; - } - - } catch (FieldAccessException e) { - e.printStackTrace(); - - // Just drop the packet - return true; - } - } - - // Silently skip players that have logged out - if (isOnline(current.getPlayer())) { - sendPacket(current); - } - } - - // Drop the packet - return true; - } - - // Add it back and stop sending - return false; - } - - /** - * Invoked when a packet has timed out. - * @param event - the timed out packet. - */ - protected abstract void onPacketTimeout(PacketEvent event); - - private boolean isOnline(Player player) { - return player != null && player.isOnline(); - } - - /** - * Send every packet, regardless of the processing state. - */ - private void forceSend() { - while (true) { - PacketEventHolder holder = sendingQueue.poll(); - - if (holder != null) { - sendPacket(holder.getEvent()); - } else { - break; - } - } - } - - /** - * Whether or not the packet transmission must synchronize with the main thread. - * @return TRUE if it must, FALSE otherwise. - */ - public boolean isSynchronizeMain() { - return notThreadSafe; - } - - /** - * Transmit a packet, if it hasn't already. - * @param event - the packet to transmit. - */ - private void sendPacket(PacketEvent event) { - - AsyncMarker marker = event.getAsyncMarker(); - - try { - // Don't send a packet twice - if (marker != null && !marker.isTransmitted()) { - marker.sendPacket(event); - } - - } catch (PlayerLoggedOutException e) { - System.out.println(String.format( - "[ProtocolLib] Warning: Dropped packet index %s of ID %s", - marker.getOriginalSendingIndex(), event.getPacketID() - )); - - } catch (IOException e) { - // Just print the error - e.printStackTrace(); - } - } - - /** - * Automatically transmits every delayed packet. - */ - public void cleanupAll() { - if (!cleanedUp) { - // Note that the cleanup itself will always occur on the main thread - forceSend(); - - // And we're done - cleanedUp = true; - } - } -} +/* + * 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.async; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.PriorityBlockingQueue; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Represents packets ready to be transmitted to a client. + * @author Kristian + */ +abstract class PacketSendingQueue { + + public static final int INITIAL_CAPACITY = 10; + + private PriorityBlockingQueue sendingQueue; + + // Asynchronous packet sending + private Executor asynchronousSender; + + // Whether or not packet transmission must occur on a specific thread + private final boolean notThreadSafe; + + // Whether or not we've run the cleanup procedure + private boolean cleanedUp = false; + + /** + * Create a packet sending queue. + * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. + */ + public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { + this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY); + this.notThreadSafe = notThreadSafe; + this.asynchronousSender = asynchronousSender; + } + + /** + * Number of packet events in the queue. + * @return The number of packet events in the queue. + */ + public int size() { + return sendingQueue.size(); + } + + /** + * Enqueue a packet for sending. + * @param packet - packet to queue. + */ + public void enqueue(PacketEvent packet) { + sendingQueue.add(new PacketEventHolder(packet)); + } + + /** + * Invoked when one of the packets have finished processing. + * @param packetUpdated - the packet that has now been updated. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { + + AsyncMarker marker = packetUpdated.getAsyncMarker(); + + // Should we reorder the event? + if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { + PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); + + // "Cancel" the original event + packetUpdated.setReadOnly(false); + packetUpdated.setCancelled(true); + + // Enqueue the copy with the new sending index + enqueue(copy); + } + + // Mark this packet as finished + marker.setProcessed(true); + trySendPackets(onMainThread); + } + + /*** + * Invoked when a list of packet IDs are no longer associated with any listeners. + * @param packetsRemoved - packets that no longer have any listeners. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { + + Set lookup = new HashSet(packetsRemoved); + + // Note that this is O(n), so it might be expensive + for (PacketEventHolder holder : sendingQueue) { + PacketEvent event = holder.getEvent(); + + if (lookup.contains(event.getPacketID())) { + event.getAsyncMarker().setProcessed(true); + } + } + + // This is likely to have changed the situation a bit + trySendPackets(onMainThread); + } + + /** + * Attempt to send any remaining packets. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public void trySendPackets(boolean onMainThread) { + // Whether or not to continue sending packets + boolean sending = true; + + // Transmit as many packets as we can + while (sending) { + PacketEventHolder holder = sendingQueue.poll(); + + if (holder != null) { + sending = processPacketHolder(onMainThread, holder); + + if (!sending) { + // Add it back again + sendingQueue.add(holder); + } + + } else { + // No more packets to send + sending = false; + } + } + } + + /** + * Invoked when a packet might be ready for transmission. + * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. + * @param holder - packet container. + * @return TRUE to continue sending packets, FALSE otherwise. + */ + private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { + PacketEvent current = holder.getEvent(); + AsyncMarker marker = current.getAsyncMarker(); + boolean hasExpired = marker.hasExpired(); + + // Guard in cause the queue is closed + if (cleanedUp) { + return true; + } + + // End condition? + if (marker.isProcessed() || hasExpired) { + if (hasExpired) { + // Notify timeout listeners + onPacketTimeout(current); + + // Recompute + marker = current.getAsyncMarker(); + hasExpired = marker.hasExpired(); + + // Could happen due to the timeout listeners + if (!marker.isProcessed() && !hasExpired) { + return false; + } + } + + // Is it okay to send the packet? + if (!current.isCancelled() && !hasExpired) { + // Make sure we're on the main thread + if (notThreadSafe) { + try { + boolean wantAsync = marker.isMinecraftAsync(current); + boolean wantSync = !wantAsync; + + // Wait for the next main thread heartbeat if we haven't fulfilled our promise + if (!onMainThread && wantSync) { + return false; + } + + // Let's give it what it wants + if (onMainThread && wantAsync) { + asynchronousSender.execute(new Runnable() { + @Override + public void run() { + // We know this isn't on the main thread + processPacketHolder(false, holder); + } + }); + + // Scheduler will do the rest + return true; + } + + } catch (FieldAccessException e) { + e.printStackTrace(); + + // Just drop the packet + return true; + } + } + + // Silently skip players that have logged out + if (isOnline(current.getPlayer())) { + sendPacket(current); + } + } + + // Drop the packet + return true; + } + + // Add it back and stop sending + return false; + } + + /** + * Invoked when a packet has timed out. + * @param event - the timed out packet. + */ + protected abstract void onPacketTimeout(PacketEvent event); + + private boolean isOnline(Player player) { + return player != null && player.isOnline(); + } + + /** + * Send every packet, regardless of the processing state. + */ + private void forceSend() { + while (true) { + PacketEventHolder holder = sendingQueue.poll(); + + if (holder != null) { + sendPacket(holder.getEvent()); + } else { + break; + } + } + } + + /** + * Whether or not the packet transmission must synchronize with the main thread. + * @return TRUE if it must, FALSE otherwise. + */ + public boolean isSynchronizeMain() { + return notThreadSafe; + } + + /** + * Transmit a packet, if it hasn't already. + * @param event - the packet to transmit. + */ + private void sendPacket(PacketEvent event) { + + AsyncMarker marker = event.getAsyncMarker(); + + try { + // Don't send a packet twice + if (marker != null && !marker.isTransmitted()) { + marker.sendPacket(event); + } + + } catch (PlayerLoggedOutException e) { + System.out.println(String.format( + "[ProtocolLib] Warning: Dropped packet index %s of ID %s", + marker.getOriginalSendingIndex(), event.getPacketID() + )); + + } catch (IOException e) { + // Just print the error + e.printStackTrace(); + } + } + + /** + * Automatically transmits every delayed packet. + */ + public void cleanupAll() { + if (!cleanedUp) { + // Note that the cleanup itself will always occur on the main thread + forceSend(); + + // And we're done + cleanedUp = true; + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java index 52d869db..826e6b0c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReferenceArray; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.injector.PrioritizedListener; @@ -34,10 +35,14 @@ import com.google.common.collect.Iterables; * @author Kristian */ public abstract class AbstractConcurrentListenerMultimap { - // The core of our map - private ConcurrentMap>> listeners = - new ConcurrentHashMap>>(); + private AtomicReferenceArray>> arrayListeners; + private ConcurrentMap>> mapListeners; + + public AbstractConcurrentListenerMultimap(int maximumPacketID) { + arrayListeners = new AtomicReferenceArray>>(maximumPacketID + 1); + mapListeners = new ConcurrentHashMap>>(); + } /** * Adds a listener to its requested list of packet recievers. @@ -45,7 +50,6 @@ public abstract class AbstractConcurrentListenerMultimap { * @param whitelist - the packet whitelist to use. */ public void addListener(TListener listener, ListeningWhitelist whitelist) { - PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority()); for (Integer packetID : whitelist.getWhitelist()) { @@ -55,21 +59,20 @@ public abstract class AbstractConcurrentListenerMultimap { // Add the listener to a specific packet notifcation list private void addListener(Integer packetID, PrioritizedListener listener) { - - SortedCopyOnWriteArray> list = listeners.get(packetID); + SortedCopyOnWriteArray> list = arrayListeners.get(packetID); // We don't want to create this for every lookup if (list == null) { // It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order, // which is a essential feature for our purposes. final SortedCopyOnWriteArray> value = new SortedCopyOnWriteArray>(); - - list = listeners.putIfAbsent(packetID, value); - - // We may end up creating multiple multisets, but we'll agree - // on the one to use. - if (list == null) { + + // We may end up creating multiple multisets, but we'll agree on which to use + if (arrayListeners.compareAndSet(packetID, null, value)) { + mapListeners.put(packetID, value); list = value; + } else { + list = arrayListeners.get(packetID); } } @@ -84,13 +87,11 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Every packet ID that was removed due to no listeners. */ public List removeListener(TListener listener, ListeningWhitelist whitelist) { - List removedPackets = new ArrayList(); // Again, not terribly efficient. But adding or removing listeners should be a rare event. for (Integer packetID : whitelist.getWhitelist()) { - - SortedCopyOnWriteArray> list = listeners.get(packetID); + SortedCopyOnWriteArray> list = arrayListeners.get(packetID); // Remove any listeners if (list != null) { @@ -100,7 +101,8 @@ public abstract class AbstractConcurrentListenerMultimap { list.remove(new PrioritizedListener(listener, whitelist.getPriority())); if (list.size() == 0) { - listeners.remove(packetID); + arrayListeners.set(packetID, null); + mapListeners.remove(packetID); removedPackets.add(packetID); } } @@ -120,7 +122,7 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Registered listeners. */ public Collection> getListener(int packetID) { - return listeners.get(packetID); + return arrayListeners.get(packetID); } /** @@ -128,7 +130,7 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Every listener. */ public Iterable> values() { - return Iterables.concat(listeners.values()); + return Iterables.concat(mapListeners.values()); } /** @@ -136,13 +138,15 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Registered packet ID. */ public Set keySet() { - return listeners.keySet(); + return mapListeners.keySet(); } /** * Remove all packet listeners. */ protected void clearListeners() { - listeners.clear(); + arrayListeners = new AtomicReferenceArray< + SortedCopyOnWriteArray>>(arrayListeners.length()); + mapListeners.clear(); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java index e2aaa3f6..c99e18e1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java @@ -18,6 +18,7 @@ package com.comphenix.protocol.concurrency; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -60,6 +61,16 @@ public class IntegerSet { array[element] = true; } + /** + * Add the given collection of elements to the set. + * @param packets - elements to add. + */ + public void addAll(Collection packets) { + for (Integer id : packets) { + add(id); + } + } + /** * Remove the given element from the set, or do nothing if it's already removed. * @param element - element to remove. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java new file mode 100644 index 00000000..69226d57 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java @@ -0,0 +1,96 @@ +package com.comphenix.protocol.error; + +import java.io.PrintStream; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.Report.ReportBuilder; +import com.comphenix.protocol.reflect.PrettyPrinter; + +/** + * Represents a basic error reporter that prints error reports to the standard error stream. + *

+ * Note that this implementation doesn't distinguish between {@link #reportWarning(Object, Report)} + * and {@link #reportDetailed(Object, Report)} - they both have the exact same behavior. + * @author Kristian + */ +public class BasicErrorReporter implements ErrorReporter { + private final PrintStream output; + + /** + * Construct a new basic error reporter that prints directly the standard error stream. + */ + public BasicErrorReporter() { + this(System.err); + } + + /** + * Construct a error reporter that prints to the given output stream. + * @param output - the output stream. + */ + public BasicErrorReporter(PrintStream output) { + this.output = output; + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error) { + output.println("Unhandled exception occured in " + methodName + " for " + sender.getName()); + error.printStackTrace(output); + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) { + reportMinimal(sender, methodName, error); + + // Also print parameters + printParameters(parameters); + } + + @Override + public void reportWarning(Object sender, Report report) { + // Basic warning + output.println("[" + sender.getClass().getSimpleName() + "] " + report.getReportMessage()); + + if (report.getException() != null) { + report.getException().printStackTrace(output); + } + printParameters(report.getCallerParameters()); + } + + @Override + public void reportWarning(Object sender, ReportBuilder reportBuilder) { + reportWarning(sender, reportBuilder.build()); + } + + @Override + public void reportDetailed(Object sender, Report report) { + // No difference from warning + reportWarning(sender, report); + } + + @Override + public void reportDetailed(Object sender, ReportBuilder reportBuilder) { + reportWarning(sender, reportBuilder); + } + + /** + * Print the given parameters to the standard error stream. + * @param parameters - the output parameters. + */ + private void printParameters(Object[] parameters) { + if (parameters != null && parameters.length > 0) { + output.println("Parameters: "); + + try { + for (Object parameter : parameters) { + if (parameter == null) + output.println("[NULL]"); + else + output.println(PrettyPrinter.printObject(parameter)); + } + } catch (IllegalAccessException e) { + // Damn it + e.printStackTrace(); + } + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java new file mode 100644 index 00000000..7584f5d0 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java @@ -0,0 +1,80 @@ +package com.comphenix.protocol.error; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.Report.ReportBuilder; + +/** + * Construct an error reporter that delegates to another error reporter. + * @author Kristian + */ +public class DelegatedErrorReporter implements ErrorReporter { + private final ErrorReporter delegated; + + /** + * Construct a new error reporter that forwards all reports to a given reporter. + * @param delegated - the delegated reporter. + */ + public DelegatedErrorReporter(ErrorReporter delegated) { + this.delegated = delegated; + } + + /** + * Retrieve the underlying error reporter. + * @return Underlying error reporter. + */ + public ErrorReporter getDelegated() { + return delegated; + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error) { + delegated.reportMinimal(sender, methodName, error); + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) { + delegated.reportMinimal(sender, methodName, error, parameters); + } + + @Override + public void reportWarning(Object sender, Report report) { + Report transformed = filterReport(sender, report, false); + + if (transformed != null) { + delegated.reportWarning(sender, transformed); + } + } + + @Override + public void reportDetailed(Object sender, Report report) { + Report transformed = filterReport(sender, report, true); + + if (transformed != null) { + delegated.reportDetailed(sender, transformed); + } + } + + /** + * Invoked before an error report is passed on to the underlying error reporter. + *

+ * To cancel a report, return NULL. + * @param sender - the sender component. + * @param report - the error report. + * @param detailed - whether or not the report will be displayed in detail. + * @return The report to pass on, or NULL to cancel it. + */ + protected Report filterReport(Object sender, Report report, boolean detailed) { + return report; + } + + @Override + public void reportWarning(Object sender, ReportBuilder reportBuilder) { + reportWarning(sender, reportBuilder.build()); + } + + @Override + public void reportDetailed(Object sender, ReportBuilder reportBuilder) { + reportDetailed(sender, reportBuilder.build()); + } +} 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..6953d437 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,499 @@ -/* - * 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); + } + + /** + * Report a problem with a given method and plugin, ensuring that we don't exceed the maximum number of error reports. + * @param sender - the component that observed this exception. + * @param methodName - the method name. + * @param error - the error itself. + * @return TRUE if the error was printed, FALSE if it was suppressed. + */ + 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()); + } + + /** + * Retrieve the current number of errors printed through {@link #reportDetailed(Object, Report)}. + * @return Number of errors printed. + */ + public int getErrorCount() { + return internalErrorCount.get(); + } + + /** + * Set the number of errors printed. + * @param errorCount - new number of errors printed. + */ + public void setErrorCount(int errorCount) { + internalErrorCount.set(errorCount); + } + + /** + * Retrieve the maximum number of errors we can print before we begin suppressing errors. + * @return Maximum number of errors. + */ + public int getMaxErrorCount() { + return maxErrorCount; + } + + /** + * Set the maximum number of errors we can print before we begin suppressing errors. + * @param maxErrorCount - new max count. + */ + public void setMaxErrorCount(int maxErrorCount) { + this.maxErrorCount = maxErrorCount; + } + + /** + * Adds the given global parameter. It will be included in every error report. + *

+ * Both key and value must be non-null. + * @param key - name of parameter. + * @param value - the global parameter itself. + */ + public void addGlobalParameter(String key, Object value) { + if (key == null) + throw new IllegalArgumentException("key cannot be NULL."); + if (value == null) + throw new IllegalArgumentException("value cannot be NULL."); + + globalParameters.put(key, value); + } + + /** + * Retrieve a global parameter by its key. + * @param key - key of the parameter to retrieve. + * @return The value of the global parameter, or NULL if not found. + */ + public Object getGlobalParameter(String key) { + if (key == null) + throw new IllegalArgumentException("key cannot be NULL."); + + return globalParameters.get(key); + } + + /** + * Reset all global parameters. + */ + public void clearGlobalParameters() { + globalParameters.clear(); + } + + /** + * Retrieve a set of every registered global parameter. + * @return Set of all registered global parameters. + */ + public Set globalParameters() { + return globalParameters.keySet(); + } + + /** + * Retrieve the support URL that will be added to all detailed reports. + * @return Support URL. + */ + public String getSupportURL() { + return supportURL; + } + + /** + * Set the support URL that will be added to all detailed reports. + * @param supportURL - the new support URL. + */ + public void setSupportURL(String supportURL) { + this.supportURL = supportURL; + } + + /** + * Retrieve the prefix to apply to every line in the error reports. + * @return Error report prefix. + */ + public String getPrefix() { + return prefix; + } + + /** + * Set the prefix to apply to every line in the error reports. + * @param prefix - new prefix. + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + /** + * Retrieve the current logger that is used to print all reports. + * @return The current logger. + */ + public Logger getLogger() { + return logger; + } + + /** + * Set the current logger that is used to print all reports. + * @param logger - new 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..fde3334f 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,74 @@ -/* - * 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; + +/** + * Represents an object that can forward an error {@link Report} to the display and permanent storage. + * + * @author Kristian + */ +public interface ErrorReporter { + /** + * Prints a small minimal error report regarding 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 regarding 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/events/PacketEvent.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java index c35330cb..5d43ff50 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java @@ -44,6 +44,9 @@ public class PacketEvent extends EventObject implements Cancellable { private AsyncMarker asyncMarker; private boolean asynchronous; + // Whether or not a packet event is read only + private boolean readOnly; + /** * Use the static constructors to create instances of this event. * @param source - the event source. @@ -114,6 +117,8 @@ public class PacketEvent extends EventObject implements Cancellable { * @param packet - the packet that will be sent instead. */ public void setPacket(PacketContainer packet) { + if (readOnly) + throw new IllegalStateException("The packet event is read-only."); this.packet = packet; } @@ -147,6 +152,8 @@ public class PacketEvent extends EventObject implements Cancellable { * @param cancel - TRUE if it should be cancelled, FALSE otherwise. */ public void setCancelled(boolean cancel) { + if (readOnly) + throw new IllegalStateException("The packet event is read-only."); this.cancel = cancel; } @@ -193,9 +200,34 @@ public class PacketEvent extends EventObject implements Cancellable { public void setAsyncMarker(AsyncMarker asyncMarker) { if (isAsynchronous()) throw new IllegalStateException("The marker is immutable for asynchronous events"); + if (readOnly) + throw new IllegalStateException("The packet event is read-only."); this.asyncMarker = asyncMarker; } + /** + * Determine if the current packet event is read only. + *

+ * This is used to ensure that a monitor listener doesn't accidentally alter the state of the event. However, + * it is still possible to modify the packet itself, as it would require too many resources to verify its integrity. + *

+ * Thus, the packet is considered immutable if the packet event is read only. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Set the read-only state of this packet event. + *

+ * This will be reset for every packet listener. + * @param readOnly - TRUE if it is read-only, FALSE otherwise. + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + /** * Determine if the packet event has been executed asynchronously or not. * @return TRUE if this packet event is asynchronous, FALSE otherwise. @@ -203,7 +235,7 @@ public class PacketEvent extends EventObject implements Cancellable { public boolean isAsynchronous() { return asynchronous; } - + private void writeObject(ObjectOutputStream output) throws IOException { // Default serialization output.defaultWriteObject(); 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/ListenerInvoker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java index f27b64e5..52b702fc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java @@ -1,67 +1,68 @@ -/* - * 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 com.comphenix.protocol.events.PacketEvent; - -/** - * Represents an object that initiate the packet listeners. - * - * @author Kristian - */ -public interface ListenerInvoker { - - /** - * Invokes the given packet event for every registered listener. - * @param event - the packet event to invoke. - */ - public abstract void invokePacketRecieving(PacketEvent event); - - /** - * Invokes the given packet event for every registered listener. - * @param event - the packet event to invoke. - */ - public abstract void invokePacketSending(PacketEvent event); - - /** - * Retrieve the associated ID of a packet. - * @param packet - the packet. - * @return The packet ID. - */ - public abstract int getPacketID(Object packet); - - /** - * Associate a given class with the given packet ID. Internal method. - * @param clazz - class to associate. - */ - public abstract void unregisterPacketClass(Class clazz); - - /** - * Remove a given class from the packet registry. Internal method. - * @param clazz - class to remove. - */ - public abstract void registerPacketClass(Class clazz, int packetID); - - /** - * 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 abstract Class getPacketClassFromID(int packetID, boolean forceVanilla); +/* + * 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 com.comphenix.protocol.events.PacketEvent; + +/** + * Represents an object that initiate the packet listeners. + * + * @author Kristian + */ +public interface ListenerInvoker { + + /** + * Invokes the given packet event for every registered listener. + * @param event - the packet event to invoke. + */ + public abstract void invokePacketRecieving(PacketEvent event); + + /** + * Invokes the given packet event for every registered listener. + * @param event - the packet event to invoke. + */ + public abstract void invokePacketSending(PacketEvent event); + + /** + * Retrieve the associated ID of a packet. + * @param packet - the packet. + * @return The packet ID. + */ + public abstract int getPacketID(Object packet); + + /** + * Associate a given class with the given packet ID. Internal method. + * @param clazz - class to associate. + */ + public abstract void unregisterPacketClass(Class clazz); + + /** + * Register a given class in the packet registry. Internal method. + * @param clazz - class to register. + * @param packetID - the the new associated packet ID. + */ + public abstract void registerPacketClass(Class clazz, int packetID); + + /** + * 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 abstract Class getPacketClassFromID(int packetID, boolean forceVanilla); } \ No newline at end of file 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 c5b8c0ac..5666765b 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; @@ -61,12 +63,29 @@ import com.comphenix.protocol.injector.spigot.SpigotPacketInjector; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Objects; 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 @@ -147,11 +166,22 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Spigot listener, if in use private SpigotPacketInjector spigotInjector; + // Plugin verifier + private PluginVerifier pluginVerifier; + /** * Only create instances of this class if protocol lib is disabled. - * @param unhookTask */ - public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) { + public PacketFilterManager(ClassLoader classLoader, Server server, Plugin library, DelayedSingleTask unhookTask, ErrorReporter reporter) { + this(classLoader, server, library, new MinecraftVersion(server), unhookTask, reporter); + } + + /** + * Only create instances of this class if protocol lib is disabled. + */ + public PacketFilterManager(ClassLoader classLoader, Server server, Plugin library, + MinecraftVersion mcVersion, DelayedSingleTask unhookTask, ErrorReporter reporter) { + if (reporter == null) throw new IllegalArgumentException("reporter cannot be NULL."); if (classLoader == null) @@ -170,6 +200,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok this.classLoader = classLoader; this.reporter = reporter; + // The plugin verifier + this.pluginVerifier = new PluginVerifier(library); + // Used to determine if injection is needed Predicate isInjectionNecessary = new Predicate() { @Override @@ -201,6 +234,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok classLoader(classLoader). packetListeners(packetListeners). injectionFilter(isInjectionNecessary). + version(mcVersion). buildHandler(); this.packetInjector = PacketInjectorBuilder.newBuilder(). @@ -218,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 (IllegalAccessException e) { - reporter.reportWarning(this, "Unable to initialize packet injector.", e); + } catch (FieldAccessException e) { + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_INITIALIZE_PACKET_INJECTOR).error(e)); } } @@ -259,6 +293,20 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok return ImmutableSet.copyOf(packetListeners); } + /** + * Warn of common programming mistakes. + * @param plugin - plugin to check. + */ + private void printPluginWarnings(Plugin plugin) { + switch (pluginVerifier.verify(plugin)) { + case NO_DEPEND: + reporter.reportWarning(this, Report.newBuilder(REPORT_PLUGIN_DEPEND_MISSING).messageParam(plugin.getName())); + case VALID: + // Do nothing + break; + } + } + @Override public void addPacketListener(PacketListener listener) { if (listener == null) @@ -267,6 +315,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // A listener can only be added once if (packetListeners.contains(listener)) return; + // Check plugin + printPluginWarnings(listener.getPlugin()); ListeningWhitelist sending = listener.getSendingWhitelist(); ListeningWhitelist receiving = listener.getReceivingWhitelist(); @@ -446,6 +496,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok asyncFilterManager.enqueueSyncPacket(event, event.getAsyncMarker()); // The above makes a copy of the event, so it's safe to cancel it + event.setReadOnly(false); event.setCancelled(true); } } @@ -478,10 +529,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 @@ -489,10 +539,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) + ); } } } @@ -529,6 +578,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (packetCreation.compareAndSet(false, true)) incrementPhases(GamePhase.PLAYING); + // Inform the MONITOR packets + if (!filters) { + sendingListeners.invokePacketSending( + reporter, + PacketEvent.fromServer(this, packet, reciever), + ListenerPriority.MONITOR); + } + playerInjection.sendServerPacket(reciever, packet, filters); } @@ -559,9 +616,16 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok mcPacket = event.getPacket().getHandle(); else return; + + } else { + // Let the monitors know though + recievedListeners.invokePacketSending( + reporter, + PacketEvent.fromClient(this, packet, sender), + ListenerPriority.MONITOR); } - playerInjection.processPacket(sender, mcPacket); + playerInjection.recieveClientPacket(sender, mcPacket); } @Override @@ -673,8 +737,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok try { // Let's clean up the other injection first. 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) + ); } } @@ -683,7 +750,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) + ); } } @@ -695,7 +764,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) + ); } } @@ -706,7 +777,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) + ); } } @@ -733,7 +806,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (!MinecraftReflection.isPacketClass(packet)) throw new IllegalArgumentException("The given object " + packet + " is not a packet."); - return PacketRegistry.getPacketToID().get(packet.getClass()); + Integer id = PacketRegistry.getPacketToID().get(packet.getClass()); + + if (id != null) { + return id; + } else { + throw new IllegalArgumentException( + "Unable to find associated packet of " + packet + ": Lookup returned NULL."); + } } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java new file mode 100644 index 00000000..2e66cf5d --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java @@ -0,0 +1,226 @@ +package com.comphenix.protocol.injector; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginLoadOrder; + +import com.google.common.collect.Sets; + +/** + * Determine if a plugin using ProtocolLib is correct. + * + * @author Kristian + */ +class PluginVerifier { + /** + * A named plugin cannot be found. + * @author Kristian + */ + public static class PluginNotFoundException extends RuntimeException { + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = 8956699101336877611L; + + public PluginNotFoundException() { + super(); + } + + public PluginNotFoundException(String message) { + super(message); + } + } + + public enum VerificationResult { + VALID, + + /** + * The plugin doesn't depend on ProtocolLib directly or indirectly. + */ + NO_DEPEND; + + /** + * Determine if the verification was valid. + */ + public boolean isValid() { + return this == VALID; + } + } + + /** + * Set of plugins that have been loaded after ProtocolLib. + */ + private final Set loadedAfter = new HashSet(); + + /** + * Reference to ProtocolLib. + */ + private final Plugin dependency; + + /** + * Construct a new plugin verifier. + * @param dependency - reference to ProtocolLib, a dependency we require of plugins. + */ + public PluginVerifier(Plugin dependency) { + if (dependency == null) + throw new IllegalArgumentException("dependency cannot be NULL."); + // This would screw with the assumption in hasDependency(Plugin, Plugin) + if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0) + throw new IllegalArgumentException("dependency cannot have a load directives."); + + this.dependency = dependency; + } + + /** + * Retrieve a plugin by name. + * @param pluginName - the non-null name of the plugin to retrieve. + * @return The retrieved plugin. + * @throws PluginNotFoundException If a plugin with the given name cannot be found. + */ + private Plugin getPlugin(String pluginName) { + Plugin plugin = getPluginOrDefault(pluginName); + + // Ensure that the plugin exists + if (plugin != null) + return plugin; + else + throw new PluginNotFoundException("Cannot find plugin " + pluginName); + } + + /** + * Retrieve a plugin by name. + * @param pluginName - the non-null name of the plugin to retrieve. + * @return The retrieved plugin, or NULL if not found. + */ + private Plugin getPluginOrDefault(String pluginName) { + return Bukkit.getPluginManager().getPlugin(pluginName); + } + + /** + * Performs simple verifications on the given plugin. + *

+ * Results may be cached. + * @param pluginName - the plugin to verify. + * @return A verification result. + * @throws IllegalArgumentException If plugin name is NULL. + * @throws PluginNotFoundException If a plugin with the given name cannot be found. + */ + public VerificationResult verify(String pluginName) { + if (pluginName == null) + throw new IllegalArgumentException("pluginName cannot be NULL."); + return verify(getPlugin(pluginName)); + } + + /** + * Performs simple verifications on the given plugin. + *

+ * Results may be cached. + * @param plugin - the plugin to verify. + * @return A verification result. + * @throws IllegalArgumentException If plugin name is NULL. + * @throws PluginNotFoundException If a plugin with the given name cannot be found. + */ + public VerificationResult verify(Plugin plugin) { + if (plugin == null) + throw new IllegalArgumentException("plugin cannot be NULL."); + + // Skip the load order check for ProtocolLib itself + if (!dependency.equals(plugin)) { + if (!loadedAfter.contains(plugin.getName())) { + if (verifyLoadOrder(dependency, plugin)) { + // Memorize + loadedAfter.add(plugin.getName()); + } else { + return VerificationResult.NO_DEPEND; + } + } + } + + // Everything seems to be in order + return VerificationResult.VALID; + } + + /** + * Determine if a given plugin is guarenteed to be loaded before the other. + *

+ * Note that the before plugin is assumed to have no "load" directives - that is, plugins to be + * loaded after itself. The after plugin may have "load" directives, but it is irrelevant for our purposes. + * @param beforePlugin - the plugin that is loaded first. + * @param afterPlugin - the plugin that is loaded last. + * @return TRUE if it will, FALSE if it may or must load in the opposite other. + */ + private boolean verifyLoadOrder(Plugin beforePlugin, Plugin afterPlugin) { + // If a plugin has a dependency, it will be loaded after its dependency + if (hasDependency(afterPlugin, beforePlugin)) { + return true; + } + + // No dependency - check the load order + if (beforePlugin.getDescription().getLoad() == PluginLoadOrder.STARTUP && + afterPlugin.getDescription().getLoad() == PluginLoadOrder.POSTWORLD) { + return true; + } + return false; + } + + /** + * Determine if a plugin has a given dependency, either directly or indirectly. + * @param plugin - the plugin to check. + * @param dependency - the dependency to find. + * @return TRUE if the plugin has the given dependency, FALSE otherwise. + */ + private boolean hasDependency(Plugin plugin, Plugin dependency) { + return hasDependency(plugin, dependency, Sets.newHashSet()); + } + + /** + * Convert a list to a set. + *

+ * A null list will be converted to an empty set. + * @param list - the list to convert. + * @return The converted list. + */ + private Set safeConversion(List list) { + if (list == null) + return Collections.emptySet(); + else + return Sets.newHashSet(list); + } + + // Avoid cycles. DFS. + private boolean hasDependency(Plugin plugin, Plugin dependency, Set checking) { + Set childNames = Sets.union( + safeConversion(plugin.getDescription().getDepend()), + safeConversion(plugin.getDescription().getSoftDepend()) + ); + + // Ensure that the same plugin isn't processed twice + if (!checking.add(plugin.getName())) { + throw new IllegalStateException("Cycle detected in dependency graph: " + plugin); + } + // Look for the dependency in the immediate children + if (childNames.contains(dependency.getName())) { + return true; + } + + // Recurse through their dependencies + for (String childName : childNames) { + Plugin childPlugin = getPluginOrDefault(childName); + + if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) { + return true; + } + } + + // Cross edges are permitted + checking.remove(plugin.getName()); + + // No dependency found! + return false; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java index 9c895e28..5391a948 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -19,8 +19,10 @@ package com.comphenix.protocol.injector; import java.util.Collection; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; @@ -30,6 +32,9 @@ import com.comphenix.protocol.events.PacketListener; * @author Kristian */ public final class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { + public SortedPacketListenerList() { + super(Packets.MAXIMUM_PACKET_ID); + } /** * Invokes the given packet event for every registered listener. @@ -45,7 +50,44 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu // The returned list is thread-safe for (PrioritizedListener element : list) { try { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketReceiving(event); + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e, + event.getPacket().getHandle()); + } + } + } + + /** + * Invokes the given packet event for every registered listener of the given priority. + * @param reporter - the error reporter that will be used to inform about listener exceptions. + * @param event - the packet event to invoke. + * @param priorityFilter - the required priority for a listener to be invoked. + */ + public void invokePacketRecieving(ErrorReporter reporter, PacketEvent event, ListenerPriority priorityFilter) { + Collection> list = getListener(event.getPacketID()); + + if (list == null) + return; + + for (PrioritizedListener element : list) { + try { + if (element.getPriority() == priorityFilter) { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); + element.getListener().onPacketReceiving(event); + } + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; } catch (Throwable e) { // Minecraft doesn't want your Exception. reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e, @@ -67,7 +109,13 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu for (PrioritizedListener element : list) { try { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketSending(event); + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; } catch (Throwable e) { // Minecraft doesn't want your Exception. reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e, @@ -76,4 +124,34 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu } } + /** + * Invokes the given packet event for every registered listener of the given priority. + * @param reporter - the error reporter that will be used to inform about listener exceptions. + * @param event - the packet event to invoke. + * @param priorityFilter - the required priority for a listener to be invoked. + */ + public void invokePacketSending(ErrorReporter reporter, PacketEvent event, ListenerPriority priorityFilter) { + Collection> list = getListener(event.getPacketID()); + + if (list == null) + return; + + for (PrioritizedListener element : list) { + try { + if (element.getPriority() == priorityFilter) { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); + element.getListener().onPacketSending(event); + } + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e, + event.getPacket().getHandle()); + } + } + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java index c3720b77..a844cd09 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java @@ -8,6 +8,7 @@ import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +import com.comphenix.protocol.reflect.FieldAccessException; import com.google.common.base.Preconditions; /** @@ -100,9 +101,9 @@ public class PacketInjectorBuilder { *

* Note that any non-null builder parameters must be set. * @return The created injector. - * @throws IllegalAccessException If anything goes wrong in terms of reflection. + * @throws FieldAccessException If anything goes wrong in terms of reflection. */ - public PacketInjector buildInjector() throws IllegalAccessException { + public PacketInjector buildInjector() throws FieldAccessException { initializeDefaults(); return new ProxyPacketInjector(classLoader, invoker, playerInjection, reporter); } 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 918546f4..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,228 +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.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableSet; - -/** - * Static packet registry in Minecraft. - * - * @author Kristian - */ -@SuppressWarnings("rawtypes") -public class PacketRegistry { - - // 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 (IllegalAccessException e) { - throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e); - } - } - - return packetToID; - } - - /** - * 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(); - 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(); - 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); - - } 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/ProxyPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java index 391c321f..a6744fb0 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java @@ -37,6 +37,7 @@ import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.MethodInfo; @@ -49,6 +50,85 @@ import com.comphenix.protocol.utility.MinecraftReflection; * @author Kristian */ class ProxyPacketInjector implements PacketInjector { + /** + * Represents a way to update the packet ID to class lookup table. + * @author Kristian + */ + private static interface PacketClassLookup { + public void setLookup(int packetID, Class clazz); + } + + private static class IntHashMapLookup implements PacketClassLookup { + // The "put" method that associates a packet ID with a packet class + private Method putMethod; + private Object intHashMap; + + public IntHashMapLookup() throws IllegalAccessException { + initialize(); + } + + @Override + public void setLookup(int packetID, Class clazz) { + try { + putMethod.invoke(intHashMap, packetID, clazz); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Illegal argument.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access method.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Exception occured in IntHashMap.put.", e); + } + } + + private void initialize() throws IllegalAccessException { + if (intHashMap == null) { + // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. + Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true). + getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); + + try { + intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Minecraft is incompatible.", e); + } + + // Now, get the "put" method. + putMethod = FuzzyReflection.fromObject(intHashMap). + getMethodByParameters("put", int.class, Object.class); + } + } + } + + private static class ArrayLookup implements PacketClassLookup { + private Class[] array; + + public ArrayLookup() throws IllegalAccessException { + initialize(); + } + + @Override + public void setLookup(int packetID, Class clazz) { + array[packetID] = clazz; + } + + private void initialize() throws IllegalAccessException { + FuzzyReflection reflection = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()); + + // Is there a Class array with 256 elements instead? + for (Field field : reflection.getFieldListByType(Class[].class)) { + Class[] test = (Class[]) FieldUtils.readField(field, (Object)null); + + if (test.length == 256) { + array = test; + return; + } + } + throw new IllegalArgumentException( + "Unable to find an array with the type " + Class[].class + + " in " + MinecraftReflection.getPacketClass()); + } + } + /** * Matches the readPacketData(DataInputStream) method in Packet. */ @@ -58,9 +138,7 @@ class ProxyPacketInjector implements PacketInjector { parameterCount(1). build(); - // The "put" method that associates a packet ID with a packet class - private static Method putMethod; - private static Object intHashMap; + private static PacketClassLookup lookup; // The packet filter manager private ListenerInvoker manager; @@ -78,7 +156,7 @@ class ProxyPacketInjector implements PacketInjector { private CallbackFilter filter; public ProxyPacketInjector(ClassLoader classLoader, ListenerInvoker manager, - PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException { + PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws FieldAccessException { this.classLoader = classLoader; this.manager = manager; @@ -100,20 +178,21 @@ class ProxyPacketInjector implements PacketInjector { } } - private void initialize() throws IllegalAccessException { - if (intHashMap == null) { - // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. - Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true). - getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); - + private void initialize() throws FieldAccessException { + if (lookup == null) { try { - intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true); - } catch (IllegalArgumentException e) { - throw new RuntimeException("Minecraft is incompatible.", e); + lookup = new IntHashMapLookup(); + } catch (Exception e1) { + + try { + lookup = new ArrayLookup(); + } catch (Exception e2) { + // Wow + throw new FieldAccessException(e1.getMessage() + ". Workaround failed too.", e2); + } } - // Now, get the "put" method. - putMethod = FuzzyReflection.fromObject(intHashMap).getMethodByParameters("put", int.class, Object.class); + // Should work fine now } } @@ -173,21 +252,12 @@ class ProxyPacketInjector implements PacketInjector { // Add a static reference Enhancer.registerStaticCallbacks(proxy, new Callback[] { NoOp.INSTANCE, modifierReadPacket, modifierRest }); - try { - // Override values - previous.put(packetID, old); - registry.put(proxy, packetID); - overwritten.put(packetID, proxy); - putMethod.invoke(intHashMap, packetID, proxy); - return true; - - } catch (IllegalArgumentException e) { - throw new RuntimeException("Illegal argument.", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access method.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Exception occured in IntHashMap.put.", e); - } + // Override values + previous.put(packetID, old); + registry.put(proxy, packetID); + overwritten.put(packetID, proxy); + lookup.setLookup(packetID, proxy); + return true; } @Override @@ -200,25 +270,14 @@ class ProxyPacketInjector implements PacketInjector { Map previous = PacketRegistry.getPreviousPackets(); Map overwritten = PacketRegistry.getOverwrittenPackets(); - // Use the old class definition - try { - Class old = previous.get(packetID); - Class proxy = PacketRegistry.getPacketClassFromID(packetID); - - putMethod.invoke(intHashMap, packetID, old); - previous.remove(packetID); - registry.remove(proxy); - overwritten.remove(packetID); - return true; - - // Handle some problems - } catch (IllegalArgumentException e) { - return false; - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access method.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Exception occured in IntHashMap.put.", e); - } + Class old = previous.get(packetID); + Class proxy = PacketRegistry.getPacketClassFromID(packetID); + + lookup.setLookup(packetID, old); + previous.remove(packetID); + registry.remove(proxy); + overwritten.remove(packetID); + return true; } @Override 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..4884dce0 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 @@ -21,11 +21,15 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Set; +import java.util.concurrent.ConcurrentMap; 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 com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.MapMaker; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.Enhancer; @@ -38,12 +42,16 @@ import net.sf.cglib.proxy.MethodProxy; * @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; + // Fake inverted proxy objects + private static ConcurrentMap delegateLookup = new MapMaker().weakKeys().makeMap(); + private transient PlayerInjector injector; private transient Set ignoredPackets; private transient ClassLoader classLoader; @@ -85,15 +93,10 @@ class InjectedArrayList extends ArrayList { 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(); - } + ProtocolLibrary.getErrorReporter().reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet) + ); // Failure return false; @@ -111,10 +114,7 @@ class InjectedArrayList extends ArrayList { 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. @@ -134,7 +134,7 @@ class InjectedArrayList extends ArrayList { // ect. // } Enhancer ex = new Enhancer(); - ex.setSuperclass(type); + ex.setSuperclass(MinecraftReflection.getPacketClass()); ex.setInterfaces(new Class[] { FakePacket.class } ); ex.setUseCache(true); ex.setClassLoader(classLoader); @@ -146,7 +146,10 @@ class InjectedArrayList extends ArrayList { try { // Temporarily associate the fake packet class invoker.registerPacketClass(proxyClass, packetID); - return proxyClass.newInstance(); + Object proxy = proxyClass.newInstance(); + + InjectedArrayList.registerDelegate(proxy, source); + return proxy; } catch (Exception e) { // Don't pollute the throws tree @@ -157,18 +160,33 @@ class InjectedArrayList extends ArrayList { } } + /** + * Ensure that the inverted integer proxy uses the given object as source. + * @param proxy - inverted integer proxy. + * @param source - source object. + */ + private static void registerDelegate(Object proxy, Object source) { + delegateLookup.put(proxy, source); + } + /** * Inverts the integer result of every integer method. * @author Kristian */ - private class InvertedIntegerCallback implements MethodInterceptor { + private class InvertedIntegerCallback implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + final Object delegate = delegateLookup.get(obj); + + if (delegate == null) { + throw new IllegalStateException("Unable to find delegate source for " + obj); + } + if (method.getReturnType().equals(int.class) && args.length == 0) { - Integer result = (Integer) proxy.invokeSuper(obj, args); + Integer result = (Integer) proxy.invoke(delegate, args); return -result; } else { - return proxy.invokeSuper(obj, args); + return proxy.invoke(delegate, 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/NetworkFieldInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java index 88d9beec..559c93ef 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java @@ -39,6 +39,7 @@ 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.MinecraftVersion; import com.google.common.collect.Sets; /** @@ -56,6 +57,12 @@ class NetworkFieldInjector extends PlayerInjector { // Nothing } + // After commit 336a4e00668fd2518c41242755ed6b3bdc3b0e6c (Update CraftBukkit to Minecraft 1.4.4.), + // CraftBukkit stopped redirecting map chunk and map chunk bulk packets to a separate queue. + // Thus, NetworkFieldInjector can safely handle every packet (though not perfectly - some packets + // will be slightly processed). + private MinecraftVersion safeVersion = new MinecraftVersion("1.4.4"); + // Packets to ignore private Set ignoredPackets = Sets.newSetFromMap(new ConcurrentHashMap()); @@ -99,7 +106,6 @@ class NetworkFieldInjector extends PlayerInjector { @Override public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException { - if (networkManager != null) { try { if (!filtered) { @@ -122,14 +128,19 @@ class NetworkFieldInjector extends PlayerInjector { } @Override - public UnsupportedListener checkListener(PacketListener listener) { - int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; - - // Unfortunately, we don't support chunk packets - if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { - return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported); - } else { + public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { + if (version != null && version.compareTo(safeVersion) > 0) { return null; + + } else { + int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; + + // Unfortunately, we don't support chunk packets + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { + return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported); + } else { + return null; + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java index 69acf343..2ea0da49 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java @@ -40,6 +40,7 @@ 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.TemporaryPlayerFactory; +import com.comphenix.protocol.utility.MinecraftVersion; /** * Injection method that overrides the NetworkHandler itself, and its queue-method. @@ -53,6 +54,12 @@ public class NetworkObjectInjector extends PlayerInjector { // Used to construct proxy objects private ClassLoader classLoader; + // After commit 336a4e00668fd2518c41242755ed6b3bdc3b0e6c (Update CraftBukkit to Minecraft 1.4.4.), + // CraftBukkit stopped redirecting map chunk and map chunk bulk packets to a separate queue. + // Thus, NetworkFieldInjector can safely handle every packet (though not perfectly - some packets + // will be slightly processed). + private MinecraftVersion safeVersion = new MinecraftVersion("1.4.4"); + // Shared callback filter - avoid creating a new class every time private volatile static CallbackFilter callbackFilter; @@ -117,14 +124,19 @@ public class NetworkObjectInjector extends PlayerInjector { } @Override - public UnsupportedListener checkListener(PacketListener listener) { - int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; - - // Unfortunately, we don't support chunk packets - if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { - return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported); - } else { + public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { + if (version != null && version.compareTo(safeVersion) > 0) { return null; + + } else { + int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; + + // Unfortunately, we don't support chunk packets + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { + return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported); + } else { + return null; + } } } 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 16218066..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,324 +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 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; - -/** - * Represents a player hook into the NetServerHandler class. - * - * @author Kristian - */ -class NetworkServerInjector extends PlayerInjector { - - private volatile static CallbackFilter callbackFilter; - - 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 (method.equals(sendPacket)) - 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) { - // This will be done by InjectedServerConnection instead - //copyTo(serverHandler, proxyObject); - - serverInjection.replaceServerHandler(serverHandler, proxyObject); - serverHandlerRef.setValue(proxyObject); - return true; - } else { - return false; - } - } - - 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 (clazz.getName().startsWith(MinecraftReflection.getMinecraftPackage())) - 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(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/PlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java index 8c59c15a..85669c69 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java @@ -126,11 +126,18 @@ public interface PlayerInjectionHandler { * @throws IllegalAccessException If the reflection machinery failed. * @throws InvocationTargetException If the underlying method caused an error. */ - public abstract void processPacket(Player player, Object mcPacket) + public abstract void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException; + /** + * Ensure that packet readers are informed of this player reference. + * @param player - the player to update. + */ + public abstract void updatePlayer(Player player); + /** * Determine if the given listeners are valid. + * @param version - the current Minecraft version, or NULL if unknown. * @param listeners - listeners to check. */ public abstract void checkListener(Set listeners); @@ -139,6 +146,7 @@ public interface PlayerInjectionHandler { * Determine if a listener is valid or not. *

* If not, a warning will be printed to the console. + * @param version - the current Minecraft version, or NULL if unknown. * @param listener - listener to check. */ public abstract void checkListener(PacketListener listener); 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 524bd40a..02036a07 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; - -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 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(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 - } - - /** - * Set the real Bukkit player that we will use. - * @param updatedPlayer - the real Bukkit player. - */ - 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(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/PlayerInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java index 120d5dc9..77ccbf5f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java @@ -14,6 +14,7 @@ import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager; +import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; @@ -37,6 +38,7 @@ public class PlayerInjectorBuilder { protected ListenerInvoker invoker; protected Set packetListeners; protected Server server; + protected MinecraftVersion version; /** * Set the class loader to use during class generation. @@ -107,6 +109,16 @@ public class PlayerInjectorBuilder { return this; } + /** + * Set the current Minecraft version. + * @param server - the current Minecraft version, or NULL if unknown. + * @return This builder, for chaining. + */ + public PlayerInjectorBuilder version(MinecraftVersion version) { + this.version = version; + return this; + } + /** * Called before an object is created with this builder. */ @@ -140,6 +152,6 @@ public class PlayerInjectorBuilder { return new ProxyPlayerInjectionHandler( classLoader, reporter, injectionFilter, - invoker, packetListeners, server); + invoker, packetListeners, server, version); } } 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 35020261..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,673 +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.InputStreamLookupBuilder; -import com.comphenix.protocol.injector.server.SocketInjector; -import com.comphenix.protocol.utility.MinecraftReflection; - -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; - - // 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) { - - this.classLoader = classLoader; - this.reporter = reporter; - this.invoker = invoker; - this.injectionFilter = injectionFilter; - this.packetListeners = packetListeners; - - 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(); - } - } - - /** - * 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() - )); - } - } - - /** - * Process 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 processPacket(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(); - // 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(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/injector/server/AbstractInputStreamLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java index 2ed52ae4..5fde4cdc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java @@ -1,119 +1,87 @@ -package com.comphenix.protocol.injector.server; - -import java.io.FilterInputStream; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.net.Socket; -import java.net.SocketAddress; -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; - -public abstract class AbstractInputStreamLookup { - // Used to access the inner input stream of a filtered input stream - private static Field filteredInputField; - - // Error reporter - protected final ErrorReporter reporter; - - // Reference to the server itself - protected final Server server; - - protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) { - this.reporter = reporter; - this.server = server; - } - - /** - * Retrieve the underlying input stream that is associated with a given filter input stream. - * @param filtered - the filter input stream. - * @return The underlying input stream that is being filtered. - * @throws FieldAccessException Unable to access input stream. - */ - protected static InputStream getInputStream(FilterInputStream filtered) { - if (filteredInputField == null) - filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). - getFieldByType("in", InputStream.class); - - InputStream current = filtered; - - try { - // Iterate until we find the real input stream - while (current instanceof FilterInputStream) { - current = (InputStream) FieldUtils.readField(filteredInputField, current, true); - } - return current; - } catch (IllegalAccessException e) { - throw new FieldAccessException("Cannot access filtered input field.", e); - } - } - - /** - * Inject the given server thread or dedicated connection. - * @param container - class that contains a ServerSocket field. - */ - public abstract void inject(Object container); - - /** - * Invoked when the world has loaded. - */ - public abstract void postWorldLoaded(); - - /** - * Retrieve the associated socket injector for a player. - * @param input - the indentifying filtered input stream. - * @return The socket injector we have associated with this player. - */ - public abstract SocketInjector waitSocketInjector(InputStream input); - - /** - * Retrieve an injector by its socket. - * @param socket - the socket. - * @return The socket injector. - */ - public abstract SocketInjector waitSocketInjector(Socket socket); - - /** - * Retrieve a injector by its address. - * @param address - the address of the socket. - * @return The socket injector, or NULL if not found. - */ - public abstract SocketInjector waitSocketInjector(SocketAddress address); - - /** - * Attempt to get a socket injector without blocking the thread. - * @param address - the address to lookup. - * @return The socket injector, or NULL if not found. - */ - public abstract SocketInjector peekSocketInjector(SocketAddress address); - - /** - * Associate a given socket address to the provided socket injector. - * @param address - the socket address to associate. - * @param injector - the injector. - */ - public abstract void setSocketInjector(SocketAddress address, SocketInjector injector); - - /** - * If a player can hold a reference to its parent injector, this method will update that reference. - * @param previous - the previous injector. - * @param current - the new injector. - */ - protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { - Player player = previous.getPlayer(); - - // Default implementation - if (player instanceof InjectorContainer) { - TemporaryPlayerFactory.setInjectorInPlayer(player, current); - } - } - - /** - * Invoked when the injection should be undone. - */ - public abstract void cleanupAll(); +package com.comphenix.protocol.injector.server; + +import java.io.InputStream; +import java.net.Socket; +import java.net.SocketAddress; +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.error.ErrorReporter; + +public abstract class AbstractInputStreamLookup { + // Error reporter + protected final ErrorReporter reporter; + + // Reference to the server itself + protected final Server server; + + protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) { + this.reporter = reporter; + this.server = server; + } + + /** + * Inject the given server thread or dedicated connection. + * @param container - class that contains a ServerSocket field. + */ + public abstract void inject(Object container); + + /** + * Invoked when the world has loaded. + */ + public abstract void postWorldLoaded(); + + /** + * Retrieve the associated socket injector for a player. + * @param input - the indentifying filtered input stream. + * @return The socket injector we have associated with this player. + */ + public abstract SocketInjector waitSocketInjector(InputStream input); + + /** + * Retrieve an injector by its socket. + * @param socket - the socket. + * @return The socket injector. + */ + public abstract SocketInjector waitSocketInjector(Socket socket); + + /** + * Retrieve a injector by its address. + * @param address - the address of the socket. + * @return The socket injector, or NULL if not found. + */ + public abstract SocketInjector waitSocketInjector(SocketAddress address); + + /** + * Attempt to get a socket injector without blocking the thread. + * @param address - the address to lookup. + * @return The socket injector, or NULL if not found. + */ + public abstract SocketInjector peekSocketInjector(SocketAddress address); + + /** + * Associate a given socket address to the provided socket injector. + * @param address - the socket address to associate. + * @param injector - the injector. + */ + public abstract void setSocketInjector(SocketAddress address, SocketInjector injector); + + /** + * If a player can hold a reference to its parent injector, this method will update that reference. + * @param previous - the previous injector. + * @param current - the new injector. + */ + protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { + Player player = previous.getPlayer(); + + // Default implementation + if (player instanceof InjectorContainer) { + TemporaryPlayerFactory.setInjectorInPlayer(player, current); + } + } + + /** + * Invoked when the injection should be undone. + */ + public abstract void cleanupAll(); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java new file mode 100644 index 00000000..8e72db01 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java @@ -0,0 +1,103 @@ +package com.comphenix.protocol.injector.server; + +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.bukkit.entity.Player; + +public class BukkitSocketInjector implements SocketInjector { + /** + * Represents a single send packet command. + * @author Kristian + */ + static class SendPacketCommand { + private final Object packet; + private final boolean filtered; + + public SendPacketCommand(Object packet, boolean filtered) { + this.packet = packet; + this.filtered = filtered; + } + + public Object getPacket() { + return packet; + } + + public boolean isFiltered() { + return filtered; + } + } + + private Player player; + + // Queue of server packets + private List syncronizedQueue = Collections.synchronizedList(new ArrayList()); + + /** + * Represents a temporary socket injector. + * @param temporaryPlayer - + */ + public BukkitSocketInjector(Player player) { + if (player == null) + throw new IllegalArgumentException("Player cannot be NULL."); + this.player = player; + } + + @Override + public Socket getSocket() throws IllegalAccessException { + throw new UnsupportedOperationException("Cannot get socket from Bukkit player."); + } + + @Override + public SocketAddress getAddress() throws IllegalAccessException { + return player.getAddress(); + } + + @Override + public void disconnect(String message) throws InvocationTargetException { + player.kickPlayer(message); + } + + @Override + public void sendServerPacket(Object packet, boolean filtered) + throws InvocationTargetException { + SendPacketCommand command = new SendPacketCommand(packet, filtered); + + // Queue until we can find something better + syncronizedQueue.add(command); + } + + @Override + public Player getPlayer() { + return player; + } + + @Override + public Player getUpdatedPlayer() { + return player; + } + + @Override + public void transferState(SocketInjector delegate) { + // Transmit all queued packets to a different injector. + try { + synchronized(syncronizedQueue) { + for (SendPacketCommand command : syncronizedQueue) { + delegate.sendServerPacket(command.getPacket(), command.isFiltered()); + } + syncronizedQueue.clear(); + } + } catch (InvocationTargetException e) { + throw new RuntimeException("Unable to transmit packets to " + delegate + " from old injector.", e); + } + } + + @Override + public void setUpdatedPlayer(Player updatedPlayer) { + this.player = updatedPlayer; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java index f374e9e9..37f5d6b6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java @@ -1,164 +1,191 @@ -package com.comphenix.protocol.injector.server; - -import java.io.FilterInputStream; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.net.Socket; -import java.net.SocketAddress; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; - -import org.bukkit.Server; - -import com.comphenix.protocol.concurrency.BlockingHashMap; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.google.common.collect.MapMaker; - -class InputStreamReflectLookup extends AbstractInputStreamLookup { - // The default lookup timeout - private static final long DEFAULT_TIMEOUT = 2000; // ms - - // Using weak keys and values ensures that we will not hold up garbage collection - protected BlockingHashMap addressLookup = new BlockingHashMap(); - protected ConcurrentMap inputLookup = new MapMaker().weakValues().makeMap(); - - // The timeout - private final long injectorTimeout; - - public InputStreamReflectLookup(ErrorReporter reporter, Server server) { - this(reporter, server, DEFAULT_TIMEOUT); - } - - /** - * Initialize a reflect lookup with a given default injector timeout. - *

- * This timeout defines the maximum amount of time to wait until an injector has been discovered. - * @param reporter - the error reporter. - * @param server - the current Bukkit server. - * @param injectorTimeout - the injector timeout. - */ - public InputStreamReflectLookup(ErrorReporter reporter, Server server, long injectorTimeout) { - super(reporter, server); - this.injectorTimeout = injectorTimeout; - } - - @Override - public void inject(Object container) { - // Do nothing - } - - @Override - public void postWorldLoaded() { - // Nothing again - } - - @Override - public SocketInjector peekSocketInjector(SocketAddress address) { - try { - return addressLookup.get(address, 0, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - // Whatever - return null; - } - } - - @Override - public SocketInjector waitSocketInjector(SocketAddress address) { - try { - // Note that we actually SWALLOW interrupts here - this is because Minecraft uses interrupts to - // periodically wake up waiting readers and writers. We have to wait for the dedicated server thread - // to catch up, so we'll swallow these interrupts. - // - // TODO: Consider if we should raise the thread priority of the dedicated server listener thread. - return addressLookup.get(address, injectorTimeout, TimeUnit.MILLISECONDS, true); - } catch (InterruptedException e) { - // This cannot be! - throw new IllegalStateException("Impossible exception occured!", e); - } - } - - @Override - public SocketInjector waitSocketInjector(Socket socket) { - return waitSocketInjector(socket.getRemoteSocketAddress()); - } - - @Override - public SocketInjector waitSocketInjector(InputStream input) { - try { - SocketAddress address = waitSocketAddress(input); - - // Guard against NPE - if (address != null) - return waitSocketInjector(address); - else - return null; - } catch (IllegalAccessException e) { - throw new FieldAccessException("Cannot find or access socket field for " + input, e); - } - } - - /** - * Use reflection to get the underlying socket address from an input stream. - * @param stream - the socket stream to lookup. - * @return The underlying socket address, or NULL if not found. - * @throws IllegalAccessException Unable to access socket field. - */ - private SocketAddress waitSocketAddress(InputStream stream) throws IllegalAccessException { - // Extra check, just in case - if (stream instanceof FilterInputStream) - return waitSocketAddress(getInputStream((FilterInputStream) stream)); - - SocketAddress result = inputLookup.get(stream); - - if (result == null) { - Socket socket = lookupSocket(stream); - - // Save it - result = socket.getRemoteSocketAddress(); - inputLookup.put(stream, result); - } - return result; - } - - @Override - public void setSocketInjector(SocketAddress address, SocketInjector injector) { - if (address == null) - throw new IllegalArgumentException("address cannot be NULL"); - if (injector == null) - throw new IllegalArgumentException("injector cannot be NULL."); - - SocketInjector previous = addressLookup.put(address, injector); - - // Any previous temporary players will also be associated - if (previous != null) { - // Update the reference to any previous injector - onPreviousSocketOverwritten(previous, injector); - } - } - - @Override - public void cleanupAll() { - // Do nothing - } - - /** - * Lookup the underlying socket of a stream through reflection. - * @param stream - the socket stream. - * @return The underlying socket. - * @throws IllegalAccessException If reflection failed. - */ - private static Socket lookupSocket(InputStream stream) throws IllegalAccessException { - if (stream instanceof FilterInputStream) { - return lookupSocket(getInputStream((FilterInputStream) stream)); - } else { - // Just do it - Field socketField = FuzzyReflection.fromObject(stream, true). - getFieldByType("socket", Socket.class); - - return (Socket) FieldUtils.readField(socketField, stream, true); - } - } -} +package com.comphenix.protocol.injector.server; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import org.bukkit.Server; + +import com.comphenix.protocol.concurrency.BlockingHashMap; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.collect.MapMaker; + +class InputStreamReflectLookup extends AbstractInputStreamLookup { + // Used to access the inner input stream of a filtered input stream + private static Field filteredInputField; + + // The default lookup timeout + private static final long DEFAULT_TIMEOUT = 2000; // ms + + // Using weak keys and values ensures that we will not hold up garbage collection + protected BlockingHashMap addressLookup = new BlockingHashMap(); + protected ConcurrentMap inputLookup = new MapMaker().weakValues().makeMap(); + + // The timeout + private final long injectorTimeout; + + public InputStreamReflectLookup(ErrorReporter reporter, Server server) { + this(reporter, server, DEFAULT_TIMEOUT); + } + + /** + * Initialize a reflect lookup with a given default injector timeout. + *

+ * This timeout defines the maximum amount of time to wait until an injector has been discovered. + * @param reporter - the error reporter. + * @param server - the current Bukkit server. + * @param injectorTimeout - the injector timeout. + */ + public InputStreamReflectLookup(ErrorReporter reporter, Server server, long injectorTimeout) { + super(reporter, server); + this.injectorTimeout = injectorTimeout; + } + + @Override + public void inject(Object container) { + // Do nothing + } + + @Override + public void postWorldLoaded() { + // Nothing again + } + + @Override + public SocketInjector peekSocketInjector(SocketAddress address) { + try { + return addressLookup.get(address, 0, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Whatever + return null; + } + } + + @Override + public SocketInjector waitSocketInjector(SocketAddress address) { + try { + // Note that we actually SWALLOW interrupts here - this is because Minecraft uses interrupts to + // periodically wake up waiting readers and writers. We have to wait for the dedicated server thread + // to catch up, so we'll swallow these interrupts. + // + // TODO: Consider if we should raise the thread priority of the dedicated server listener thread. + return addressLookup.get(address, injectorTimeout, TimeUnit.MILLISECONDS, true); + } catch (InterruptedException e) { + // This cannot be! + throw new IllegalStateException("Impossible exception occured!", e); + } + } + + @Override + public SocketInjector waitSocketInjector(Socket socket) { + return waitSocketInjector(socket.getRemoteSocketAddress()); + } + + @Override + public SocketInjector waitSocketInjector(InputStream input) { + try { + SocketAddress address = waitSocketAddress(input); + + // Guard against NPE + if (address != null) + return waitSocketInjector(address); + else + return null; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot find or access socket field for " + input, e); + } + } + + /** + * Use reflection to get the underlying socket address from an input stream. + * @param stream - the socket stream to lookup. + * @return The underlying socket address, or NULL if not found. + * @throws IllegalAccessException Unable to access socket field. + */ + private SocketAddress waitSocketAddress(InputStream stream) throws IllegalAccessException { + // Extra check, just in case + if (stream instanceof FilterInputStream) + return waitSocketAddress(getInputStream((FilterInputStream) stream)); + + SocketAddress result = inputLookup.get(stream); + + if (result == null) { + Socket socket = lookupSocket(stream); + + // Save it + result = socket.getRemoteSocketAddress(); + inputLookup.put(stream, result); + } + return result; + } + + /** + * Retrieve the underlying input stream that is associated with a given filter input stream. + * @param filtered - the filter input stream. + * @return The underlying input stream that is being filtered. + * @throws FieldAccessException Unable to access input stream. + */ + protected static InputStream getInputStream(FilterInputStream filtered) { + if (filteredInputField == null) + filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). + getFieldByType("in", InputStream.class); + + InputStream current = filtered; + + try { + // Iterate until we find the real input stream + while (current instanceof FilterInputStream) { + current = (InputStream) FieldUtils.readField(filteredInputField, current, true); + } + return current; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access filtered input field.", e); + } + } + + @Override + public void setSocketInjector(SocketAddress address, SocketInjector injector) { + if (address == null) + throw new IllegalArgumentException("address cannot be NULL"); + if (injector == null) + throw new IllegalArgumentException("injector cannot be NULL."); + + SocketInjector previous = addressLookup.put(address, injector); + + // Any previous temporary players will also be associated + if (previous != null) { + // Update the reference to any previous injector + onPreviousSocketOverwritten(previous, injector); + } + } + + @Override + public void cleanupAll() { + // Do nothing + } + + /** + * Lookup the underlying socket of a stream through reflection. + * @param stream - the socket stream. + * @return The underlying socket. + * @throws IllegalAccessException If reflection failed. + */ + private static Socket lookupSocket(InputStream stream) throws IllegalAccessException { + if (stream instanceof FilterInputStream) { + return lookupSocket(getInputStream((FilterInputStream) stream)); + } else { + // Just do it + Field socketField = FuzzyReflection.fromObject(stream, true). + getFieldByType("socket", Socket.class); + + return (Socket) FieldUtils.readField(socketField, stream, true); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java index 6407d320..e484c2e5 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java @@ -58,4 +58,10 @@ public interface SocketInjector { * @param delegate - the new injector. */ public abstract void transferState(SocketInjector delegate); + + /** + * Set the real Bukkit player that we will use. + * @param updatedPlayer - the real Bukkit player. + */ + public abstract void setUpdatedPlayer(Player updatedPlayer); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java index 91d1fed6..8c2e0e05 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java @@ -1,195 +1,197 @@ -/* - * 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.server; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.CallbackFilter; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; -import net.sf.cglib.proxy.NoOp; - -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.injector.PacketConstructor; -import com.comphenix.protocol.reflect.FieldAccessException; - -/** - * Create fake player instances that represents pre-authenticated clients. - */ -public class TemporaryPlayerFactory { - // Helpful constructors - private final PacketConstructor chatPacket; - - // Prevent too many class creations - private static CallbackFilter callbackFilter; - - public TemporaryPlayerFactory() { - chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" }); - } - - /** - * Retrieve the injector from a given player if it contains one. - * @param player - the player that may contain a reference to a player injector. - * @return The referenced player injector, or NULL if none can be found. - */ - public static SocketInjector getInjectorFromPlayer(Player player) { - if (player instanceof InjectorContainer) { - return ((InjectorContainer) player).getInjector(); - } - return null; - } - - /** - * Set the player injector, if possible. - * @param player - the player to update. - * @param injector - the injector to store. - */ - public static void setInjectorInPlayer(Player player, SocketInjector injector) { - ((InjectorContainer) player).setInjector(injector); - } - - /** - * Construct a temporary player that supports a subset of every player command. - *

- * Supported methods include: - *

    - *
  • getPlayer()
  • - *
  • getAddress()
  • - *
  • getServer()
  • - *
  • chat(String)
  • - *
  • sendMessage(String)
  • - *
  • sendMessage(String[])
  • - *
  • kickPlayer(String)
  • - *
- *

- * Note that a temporary player has not yet been assigned a name, and thus cannot be - * uniquely identified. Use the address instead. - * @param injector - the player injector used. - * @param server - the current server. - * @return A temporary player instance. - */ - public Player createTemporaryPlayer(final Server server) { - - // Default implementation - Callback implementation = new MethodInterceptor() { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - - String methodName = method.getName(); - SocketInjector injector = ((InjectorContainer) obj).getInjector(); - - if (injector == null) - throw new IllegalStateException("Unable to find injector."); - - // Use the socket to get the address - if (methodName.equalsIgnoreCase("getName")) - return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]"; - if (methodName.equalsIgnoreCase("getPlayer")) - return injector.getUpdatedPlayer(); - if (methodName.equalsIgnoreCase("getAddress")) - return injector.getAddress(); - if (methodName.equalsIgnoreCase("getServer")) - return server; - - try { - // Handle send message methods - if (methodName.equalsIgnoreCase("chat") || methodName.equalsIgnoreCase("sendMessage")) { - Object argument = args[0]; - - // Dynamic overloading - if (argument instanceof String) { - return sendMessage(injector, (String) argument); - } else if (argument instanceof String[]) { - for (String message : (String[]) argument) { - sendMessage(injector, message); - } - return null; - } - } - } catch (InvocationTargetException e) { - throw e.getCause(); - } - - // Also, handle kicking - if (methodName.equalsIgnoreCase("kickPlayer")) { - injector.disconnect((String) args[0]); - return null; - } - - // Ignore all other methods - throw new UnsupportedOperationException( - "The method " + method.getName() + " is not supported for temporary players."); - } - }; - - // Shared callback filter - if (callbackFilter == null) { - callbackFilter = new CallbackFilter() { - @Override - public int accept(Method method) { - // Do not override the object method or the superclass methods - if (method.getDeclaringClass().equals(Object.class) || - method.getDeclaringClass().equals(InjectorContainer.class)) - return 0; - else - return 1; - } - }; - } - - // CGLib is amazing - Enhancer ex = new Enhancer(); - ex.setSuperclass(InjectorContainer.class); - ex.setInterfaces(new Class[] { Player.class }); - ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation }); - ex.setCallbackFilter(callbackFilter); - - return (Player) ex.create(); - } - - /** - * Construct a temporary player with the given associated socket injector. - * @param server - the parent server. - * @param injector - the referenced socket injector. - * @return The temporary player. - */ - public Player createTemporaryPlayer(Server server, SocketInjector injector) { - Player temporary = createTemporaryPlayer(server); - - ((InjectorContainer) temporary).setInjector(injector); - return temporary; - } - - /** - * Send a message to the given client. - * @param injector - the injector representing the client. - * @param message - a message. - * @return Always NULL. - * @throws InvocationTargetException If the message couldn't be sent. - * @throws FieldAccessException If we were unable to construct the message packet. - */ - private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException { - injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false); - 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.server; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; +import net.sf.cglib.proxy.NoOp; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.injector.PacketConstructor; +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Create fake player instances that represents pre-authenticated clients. + */ +public class TemporaryPlayerFactory { + // Helpful constructors + private final PacketConstructor chatPacket; + + // Prevent too many class creations + private static CallbackFilter callbackFilter; + + public TemporaryPlayerFactory() { + chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" }); + } + + /** + * Retrieve the injector from a given player if it contains one. + * @param player - the player that may contain a reference to a player injector. + * @return The referenced player injector, or NULL if none can be found. + */ + public static SocketInjector getInjectorFromPlayer(Player player) { + if (player instanceof InjectorContainer) { + return ((InjectorContainer) player).getInjector(); + } + return null; + } + + /** + * Set the player injector, if possible. + * @param player - the player to update. + * @param injector - the injector to store. + */ + public static void setInjectorInPlayer(Player player, SocketInjector injector) { + ((InjectorContainer) player).setInjector(injector); + } + + /** + * Construct a temporary player that supports a subset of every player command. + *

+ * Supported methods include: + *

    + *
  • getPlayer()
  • + *
  • getAddress()
  • + *
  • getServer()
  • + *
  • chat(String)
  • + *
  • sendMessage(String)
  • + *
  • sendMessage(String[])
  • + *
  • kickPlayer(String)
  • + *
+ *

+ * Note that a temporary player has not yet been assigned a name, and thus cannot be + * uniquely identified. Use the address instead. + * @param injector - the player injector used. + * @param server - the current server. + * @return A temporary player instance. + */ + public Player createTemporaryPlayer(final Server server) { + + // Default implementation + Callback implementation = new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + + String methodName = method.getName(); + SocketInjector injector = ((InjectorContainer) obj).getInjector(); + + if (injector == null) + throw new IllegalStateException("Unable to find injector."); + + // Use the socket to get the address + if (methodName.equalsIgnoreCase("isOnline")) + return injector.getSocket() != null && injector.getSocket().isConnected(); + if (methodName.equalsIgnoreCase("getName")) + return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]"; + if (methodName.equalsIgnoreCase("getPlayer")) + return injector.getUpdatedPlayer(); + if (methodName.equalsIgnoreCase("getAddress")) + return injector.getAddress(); + if (methodName.equalsIgnoreCase("getServer")) + return server; + + try { + // Handle send message methods + if (methodName.equalsIgnoreCase("chat") || methodName.equalsIgnoreCase("sendMessage")) { + Object argument = args[0]; + + // Dynamic overloading + if (argument instanceof String) { + return sendMessage(injector, (String) argument); + } else if (argument instanceof String[]) { + for (String message : (String[]) argument) { + sendMessage(injector, message); + } + return null; + } + } + } catch (InvocationTargetException e) { + throw e.getCause(); + } + + // Also, handle kicking + if (methodName.equalsIgnoreCase("kickPlayer")) { + injector.disconnect((String) args[0]); + return null; + } + + // Ignore all other methods + throw new UnsupportedOperationException( + "The method " + method.getName() + " is not supported for temporary players."); + } + }; + + // Shared callback filter + if (callbackFilter == null) { + callbackFilter = new CallbackFilter() { + @Override + public int accept(Method method) { + // Do not override the object method or the superclass methods + if (method.getDeclaringClass().equals(Object.class) || + method.getDeclaringClass().equals(InjectorContainer.class)) + return 0; + else + return 1; + } + }; + } + + // CGLib is amazing + Enhancer ex = new Enhancer(); + ex.setSuperclass(InjectorContainer.class); + ex.setInterfaces(new Class[] { Player.class }); + ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation }); + ex.setCallbackFilter(callbackFilter); + + return (Player) ex.create(); + } + + /** + * Construct a temporary player with the given associated socket injector. + * @param server - the parent server. + * @param injector - the referenced socket injector. + * @return The temporary player. + */ + public Player createTemporaryPlayer(Server server, SocketInjector injector) { + Player temporary = createTemporaryPlayer(server); + + ((InjectorContainer) temporary).setInjector(injector); + return temporary; + } + + /** + * Send a message to the given client. + * @param injector - the injector representing the client. + * @param message - a message. + * @return Always NULL. + * @throws InvocationTargetException If the message couldn't be sent. + * @throws FieldAccessException If we were unable to construct the message packet. + */ + private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException { + injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false); + return null; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java index dc6f5e9d..8df7f70c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java @@ -74,7 +74,7 @@ class DummyPlayerHandler implements PlayerInjectionHandler { } @Override - public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { injector.processPacket(player, mcPacket); } @@ -119,4 +119,9 @@ class DummyPlayerHandler implements PlayerInjectionHandler { public void postWorldLoaded() { // Do nothing } + + @Override + public void updatePlayer(Player player) { + // Do nothing + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java index 30d68021..555a5304 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java @@ -22,7 +22,9 @@ import net.sf.cglib.proxy.NoOp; import com.comphenix.protocol.Packets; import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.DelegatedErrorReporter; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.ListenerInvoker; @@ -47,7 +49,7 @@ public class SpigotPacketInjector implements SpigotPacketListener { private static volatile boolean classChecked; // Retrieve the entity player from a PlayerConnection - private static Field playerConnectionPlayer; + private static volatile Field playerConnectionPlayer; // Packets that are not to be processed by the filters private Set ignoredPackets = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); @@ -275,7 +277,8 @@ public class SpigotPacketInjector implements SpigotPacketListener { if (dummyInjector == null) { // Inject the network manager try { - NetworkObjectInjector created = new NetworkObjectInjector(classLoader, reporter, null, invoker, null); + NetworkObjectInjector created = new NetworkObjectInjector( + classLoader, filterImpossibleWarnings(reporter), null, invoker, null); if (MinecraftReflection.isLoginHandler(connection)) { created.initialize(connection); @@ -303,6 +306,23 @@ public class SpigotPacketInjector implements SpigotPacketListener { return dummyInjector; } + /** + * Return a delegated error reporter that ignores certain warnings that are irrelevant on Spigot. + * @param reporter - error reporter to delegate. + * @return The filtered error reporter. + */ + private ErrorReporter filterImpossibleWarnings(ErrorReporter reporter) { + return new DelegatedErrorReporter(reporter) { + @Override + protected Report filterReport(Object sender, Report report, boolean detailed) { + // This doesn't matter - ignore it + if (report.getType() == NetworkObjectInjector.REPORT_DETECTED_CUSTOM_SERVER_HANDLER) + return null; + return report; + } + }; + } + /** * Save a given player injector for later. * @param networkManager - the associated network manager. @@ -400,7 +420,8 @@ public class SpigotPacketInjector implements SpigotPacketListener { */ void injectPlayer(Player player) { try { - NetworkObjectInjector dummy = new NetworkObjectInjector(classLoader, reporter, player, invoker, null); + NetworkObjectInjector dummy = new NetworkObjectInjector( + classLoader, filterImpossibleWarnings(reporter), player, invoker, null); dummy.initializePlayer(player); // Save this player for the network manager 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 836a45d1..19e9e56c 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,360 +1,365 @@ -/* - * 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); - } - - // 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 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, ErrorReporter reporter, ExecutorService executor) { + if (loader == null) + throw new IllegalArgumentException("loader cannot be NULL"); + if (executor == null) + throw new IllegalArgumentException("executor cannot be NULL"); + if (reporter == null) + throw new IllegalArgumentException("reporter 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 + reporter.reportDetailed(BackgroundCompiler.this, + Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e) + ); + } + + // 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(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java index a93ea18f..0e522142 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java @@ -62,6 +62,25 @@ public abstract class AbstractFuzzyMatcher implements Comparable obj) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java index bb1bfcdb..3a15ab44 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java @@ -1,6 +1,7 @@ package com.comphenix.protocol.reflect.fuzzy; import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -23,6 +24,9 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { private final ImmutableList> methodContracts; private final ImmutableList> constructorContracts; + private final ImmutableList>> baseclassContracts; + private final ImmutableList>> interfaceContracts; + /** * Represents a class contract builder. * @author Kristian @@ -33,6 +37,9 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { private List> methodContracts = Lists.newArrayList(); private List> constructorContracts = Lists.newArrayList(); + private List>> baseclassContracts = Lists.newArrayList(); + private List>> interfaceContracts = Lists.newArrayList(); + /** * Add a new field contract. * @param matcher - new field contract. @@ -89,18 +96,54 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { public Builder constructor(FuzzyMethodContract.Builder builder) { return constructor(builder.build()); } + + /** + * Add a new base class contract. + * @param matcher - new base class contract. + * @return This builder, for chaining. + */ + public Builder baseclass(AbstractFuzzyMatcher> matcher) { + baseclassContracts.add(matcher); + return this; + } + + /** + * Add a new base class contract. + * @param matcher - builder for the new base class contract. + * @return This builder, for chaining. + */ + public Builder baseclass(FuzzyClassContract.Builder builder) { + return baseclass(builder.build()); + } + + /** + * Add a new interface contract. + * @param matcher - new interface contract. + * @return This builder, for chaining. + */ + public Builder interfaces(AbstractFuzzyMatcher> matcher) { + interfaceContracts.add(matcher); + return this; + } + + /** + * Add a new interface contract. + * @param matcher - builder for the new interface contract. + * @return This builder, for chaining. + */ + public Builder interfaces(FuzzyClassContract.Builder builder) { + return interfaces(builder.build()); + } public FuzzyClassContract build() { Collections.sort(fieldContracts); Collections.sort(methodContracts); Collections.sort(constructorContracts); + Collections.sort(baseclassContracts); + Collections.sort(interfaceContracts); // Construct a new class matcher - return new FuzzyClassContract( - ImmutableList.copyOf(fieldContracts), - ImmutableList.copyOf(methodContracts), - ImmutableList.copyOf(constructorContracts) - ); + return new FuzzyClassContract(this); } } @@ -114,17 +157,15 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { /** * Constructs a new fuzzy class contract with the given contracts. - * @param fieldContracts - field contracts. - * @param methodContracts - method contracts. - * @param constructorContracts - constructor contracts. + * @param builder - the builder that is constructing us. */ - private FuzzyClassContract(ImmutableList> fieldContracts, - ImmutableList> methodContracts, - ImmutableList> constructorContracts) { + private FuzzyClassContract(Builder builder) { super(); - this.fieldContracts = fieldContracts; - this.methodContracts = methodContracts; - this.constructorContracts = constructorContracts; + this.fieldContracts = ImmutableList.copyOf(builder.fieldContracts); + this.methodContracts = ImmutableList.copyOf(builder.methodContracts); + this.constructorContracts = ImmutableList.copyOf(builder.constructorContracts); + this.baseclassContracts = ImmutableList.copyOf(builder.baseclassContracts); + this.interfaceContracts = ImmutableList.copyOf(builder.interfaceContracts); } /** @@ -157,12 +198,34 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { return constructorContracts; } + /** + * Retrieve an immutable list of every baseclass contract. + *

+ * This list is ordered in descending order of priority. + * @return List of every baseclass contract. + */ + public ImmutableList>> getBaseclassContracts() { + return baseclassContracts; + } + + /** + * Retrieve an immutable list of every interface contract. + *

+ * This list is ordered in descending order of priority. + * @return List of every interface contract. + */ + public ImmutableList>> getInterfaceContracts() { + return interfaceContracts; + } + @Override protected int calculateRoundNumber() { // Find the highest round number return combineRounds(findHighestRound(fieldContracts), - combineRounds(findHighestRound(methodContracts), - findHighestRound(constructorContracts))); + findHighestRound(methodContracts), + findHighestRound(constructorContracts), + findHighestRound(interfaceContracts), + findHighestRound(baseclassContracts)); } private int findHighestRound(Collection> list) { @@ -179,12 +242,19 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { FuzzyReflection reflection = FuzzyReflection.fromClass(value, true); // Make sure all the contracts are valid - return processContracts(reflection.getFields(), value, fieldContracts) && - processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts) && - processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts); + return (fieldContracts.size() == 0 || + processContracts(reflection.getFields(), value, fieldContracts)) && + (methodContracts.size() == 0 || + processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts)) && + (constructorContracts.size() == 0 || + processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts)) && + (baseclassContracts.size() == 0 || + processValue(value.getSuperclass(), parent, baseclassContracts)) && + (interfaceContracts.size() == 0 || + processContracts(Arrays.asList(value.getInterfaces()), (Class) parent, interfaceContracts)); } - private boolean processContracts(Collection values, Class parent, List> matchers) { + private boolean processContracts(Collection values, Object parent, List> matchers) { boolean[] accepted = new boolean[matchers.size()]; int count = accepted.length; @@ -205,7 +275,18 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { return count == 0; } - private int processValue(T value, Class parent, boolean accepted[], List> matchers) { + private boolean processValue(T value, Object parent, List> matchers) { + for (int i = 0; i < matchers.size(); i++) { + if (matchers.get(i).isMatch(value, parent)) { + return true; + } + } + + // No match + return false; + } + + private int processValue(T value, Object parent, boolean accepted[], List> matchers) { // The order matters for (int i = 0; i < matchers.size(); i++) { if (!accepted[i]) { @@ -235,6 +316,12 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { if (constructorContracts.size() > 0) { params.put("constructors", constructorContracts); } + if (baseclassContracts.size() > 0) { + params.put("baseclasses", baseclassContracts); + } + if (interfaceContracts.size() > 0) { + params.put("interfaces", interfaceContracts); + } return "{\n " + Joiner.on(", \n ").join(params.entrySet()) + "\n}"; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java index 826a4ecd..f5fa9e54 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -327,6 +327,7 @@ public class DefaultInstances implements InstanceProvider { try { return (T) constructor.newInstance(params); } catch (Exception e) { + //e.printStackTrace(); // Cannot create it return null; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 399082d1..9261f7bb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -81,6 +81,9 @@ public class MinecraftReflection { private static Constructor craftNMSConstructor; private static Constructor craftBukkitConstructor; + // Matches classes + private static AbstractFuzzyMatcher> fuzzyMatcher; + // New in 1.4.5 private static Method craftNMSMethod; private static Method craftBukkitMethod; @@ -88,7 +91,12 @@ public class MinecraftReflection { // net.minecraft.server private static Class itemStackArrayClass; - + + /** + * Whether or not we're currently initializing the reflection handler. + */ + private static boolean initializing; + private MinecraftReflection() { // No need to make this constructable. } @@ -108,7 +116,9 @@ public class MinecraftReflection { * @return A matcher for Minecraft objects. */ public static AbstractFuzzyMatcher> getMinecraftObjectMatcher() { - return FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50); + if (fuzzyMatcher == null) + fuzzyMatcher = FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50); + return fuzzyMatcher; } /** @@ -119,6 +129,9 @@ public class MinecraftReflection { // Speed things up if (MINECRAFT_FULL_PACKAGE != null) return MINECRAFT_FULL_PACKAGE; + if (initializing) + throw new IllegalStateException("Already initializing minecraft package!"); + initializing = true; Server craftServer = Bukkit.getServer(); @@ -129,6 +142,9 @@ public class MinecraftReflection { Class craftClass = craftServer.getClass(); CRAFTBUKKIT_PACKAGE = getPackage(craftClass.getCanonicalName()); + // Libigot patch + handleLibigot(); + // Next, do the same for CraftEntity.getHandle() in order to get the correct Minecraft package Class craftEntity = getCraftEntityClass(); Method getHandle = craftEntity.getMethod("getHandle"); @@ -141,16 +157,16 @@ public class MinecraftReflection { MINECRAFT_PREFIX_PACKAGE = MINECRAFT_FULL_PACKAGE; // The package is usualy flat, so go with that assumtion - DYNAMIC_PACKAGE_MATCHER = + String matcher = (MINECRAFT_PREFIX_PACKAGE.length() > 0 ? Pattern.quote(MINECRAFT_PREFIX_PACKAGE + ".") : "") + "\\w+"; // We'll still accept the default location, however - DYNAMIC_PACKAGE_MATCHER = "(" + DYNAMIC_PACKAGE_MATCHER + ")|(" + MINECRAFT_OBJECT + ")"; + setDynamicPackageMatcher("(" + matcher + ")|(" + MINECRAFT_OBJECT + ")"); } else { // Use the standard matcher - DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT; + setDynamicPackageMatcher(MINECRAFT_OBJECT); } return MINECRAFT_FULL_PACKAGE; @@ -159,12 +175,40 @@ public class MinecraftReflection { throw new RuntimeException("Security violation. Cannot get handle method.", e); } catch (NoSuchMethodException e) { throw new IllegalStateException("Cannot find getHandle() method on server. Is this a modified CraftBukkit version?", e); + } finally { + initializing = false; } } else { + initializing = false; throw new IllegalStateException("Could not find Bukkit. Is it running?"); } } + + /** + * Update the dynamic package matcher. + * @param regex - the Minecraft package regex. + */ + private static void setDynamicPackageMatcher(String regex) { + DYNAMIC_PACKAGE_MATCHER = regex; + + // Ensure that the matcher is regenerated + fuzzyMatcher = null; + } + + // Patch for Libigot + private static void handleLibigot() { + try { + getCraftEntityClass(); + } catch (RuntimeException e) { + // Try reverting the package to the old format + craftbukkitPackage = null; + CRAFTBUKKIT_PACKAGE = "org.bukkit.craftbukkit"; + + // This might fail too + getCraftEntityClass(); + } + } /** * Used during debugging and testing. @@ -176,7 +220,7 @@ public class MinecraftReflection { CRAFTBUKKIT_PACKAGE = craftBukkitPackage; // Standard matcher - DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT; + setDynamicPackageMatcher(MINECRAFT_OBJECT); } /** @@ -244,8 +288,7 @@ public class MinecraftReflection { if (clazz == null) throw new IllegalArgumentException("Class cannot be NULL."); - // Doesn't matter if we don't check for the version here - return clazz.getName().startsWith(MINECRAFT_PREFIX_PACKAGE); + return getMinecraftObjectMatcher().isMatch(clazz, null); } /** @@ -809,9 +852,15 @@ public class MinecraftReflection { returnTypeMatches(tagCompoundContract). build() ); + Class nbtBase = selected.getReturnType().getSuperclass(); + // That can't be correct + if (nbtBase == null || nbtBase.equals(Object.class)) { + throw new IllegalStateException("Unable to find NBT base class: " + nbtBase); + } + // Use the return type here too - return setMinecraftClass("NBTBase", selected.getReturnType()); + return setMinecraftClass("NBTBase", nbtBase); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/MinecraftVersion.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java similarity index 98% rename from ProtocolLib/src/main/java/com/comphenix/protocol/MinecraftVersion.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java index 53fb9898..e78dc266 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/MinecraftVersion.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol; +package com.comphenix.protocol.utility; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,7 +31,7 @@ import com.google.common.collect.Ordering; * * @author Kristian */ -class MinecraftVersion implements Comparable { +public class MinecraftVersion implements Comparable { /** * Regular expression used to parse version strings. */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java new file mode 100644 index 00000000..bef4b369 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java @@ -0,0 +1,104 @@ +package com.comphenix.protocol.wrappers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; + +/** + * Wrap a GNU Trove Collection class with an equivalent Java Collection class. + * @author Kristian + */ +public class TroveWrapper { + private volatile static Class decorators; + + /** + * Retrieve a Java wrapper for the corresponding Trove map. + * @param troveMap - the trove map to wrap. + * @return The wrapped GNU Trove map. + * @throws IllegalStateException If GNU Trove cannot be found in the class map. + * @throws IllegalArgumentException If troveMap is NULL. + * @throws FieldAccessException Error in wrapper method or lack of reflection permissions. + */ + public static Map getDecoratedMap(@Nonnull Object troveMap) { + @SuppressWarnings("unchecked") + Map result = (Map) getDecorated(troveMap); + return result; + } + + /** + * Retrieve a Java wrapper for the corresponding Trove set. + * @param troveSet - the trove set to wrap. + * @return The wrapped GNU Trove set. + * @throws IllegalStateException If GNU Trove cannot be found in the class map. + * @throws IllegalArgumentException If troveSet is NULL. + * @throws FieldAccessException Error in wrapper method or lack of reflection permissions. + */ + public static Set getDecoratedSet(@Nonnull Object troveSet) { + @SuppressWarnings("unchecked") + Set result = (Set) getDecorated(troveSet); + return result; + } + + /** + * Retrieve a Java wrapper for the corresponding Trove list. + * @param troveList - the trove list to wrap. + * @return The wrapped GNU Trove list. + * @throws IllegalStateException If GNU Trove cannot be found in the class map. + * @throws IllegalArgumentException If troveList is NULL. + * @throws FieldAccessException Error in wrapper method or lack of reflection permissions. + */ + public static List getDecoratedList(@Nonnull Object troveList) { + @SuppressWarnings("unchecked") + List result = (List) getDecorated(troveList); + return result; + } + + private static Object getDecorated(@Nonnull Object trove) { + if (trove == null) + throw new IllegalArgumentException("trove instance cannot be non-null."); + + AbstractFuzzyMatcher> match = FuzzyMatchers.matchSuper(trove.getClass()); + + if (decorators == null) { + try { + // Attempt to get decorator class + decorators = TroveWrapper.class.getClassLoader().loadClass("gnu.trove.TDecorators"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Cannot find TDecorators in Gnu Trove.", e); + } + } + + // Find an appropriate wrapper method in TDecorators + for (Method method : decorators.getMethods()) { + Class[] types = method.getParameterTypes(); + + if (types.length == 1 && match.isMatch(types[0], null)) { + try { + Object result = method.invoke(null, trove); + + if (result == null) + throw new FieldAccessException("Wrapper returned NULL."); + else + return result; + + } catch (IllegalArgumentException e) { + throw new FieldAccessException("Cannot invoke wrapper method.", e); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Illegal access.", e); + } catch (InvocationTargetException e) { + throw new FieldAccessException("Error in invocation.", e); + } + } + } + + throw new IllegalArgumentException("Cannot find decorator for " + trove + " (" + trove.getClass() + ")"); + } +} diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index 46869ad7..02c7dd23 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -6,8 +6,6 @@ global: # Number of seconds to wait until a new update is downloaded delay: 43200 # 12 hours - # Last update time - last: 0 metrics: true @@ -18,4 +16,10 @@ global: ignore version check: # Override the starting injecting method - injection method: \ No newline at end of file + injection method: + + # Whether or not to enable the filter command + debug: false + + # The engine used by the filter command + script engine: JavaScript \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 23fb34bf..423ece96 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,9 +1,11 @@ name: ProtocolLib -version: 2.3.0 +version: 2.4.3 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib + main: com.comphenix.protocol.ProtocolLibrary +load: startup database: false commands: @@ -17,6 +19,12 @@ commands: usage: / add|remove|names client|server [ID start]-[ID stop] [detailed] permission: protocol.admin permission-message: You don't have + filter: + description: Add or remove programmable filters to the packet listeners. + usage: / add|remove name [ID start]-[ID stop] + aliases: [packet_filter] + permission: protocol.admin + permission-message: You don't have permissions: protocol.*: diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java b/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java index 9fac9cac..39b6b55d 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java @@ -4,8 +4,10 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import net.minecraft.server.v1_5_R2.StatisticList; + // Will have to be updated for every version though -import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemFactory; +import org.bukkit.craftbukkit.v1_5_R2.inventory.CraftItemFactory; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -34,6 +36,12 @@ public class BukkitInitialization { initializePackage(); + try { + StatisticList.b(); + } catch (Exception e) { + // Swallow + } + // Mock the server object Server mockedServer = mock(Server.class); ItemFactory mockedFactory = mock(CraftItemFactory.class); @@ -55,6 +63,6 @@ public class BukkitInitialization { */ public static void initializePackage() { // Initialize reflection - MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_4_R1", "org.bukkit.craftbukkit.v1_4_R1"); + MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_5_R2", "org.bukkit.craftbukkit.v1_5_R2"); } } diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java index 8f98de70..2d127c23 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java @@ -21,6 +21,8 @@ import static org.junit.Assert.*; import org.junit.Test; +import com.comphenix.protocol.utility.MinecraftVersion; + public class MinecraftVersionTest { @Test diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index 1b66511e..d305adf4 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -22,7 +22,7 @@ import java.lang.reflect.Array; import java.util.List; // Will have to be updated for every version though -import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemFactory; +import org.bukkit.craftbukkit.v1_5_R2.inventory.CraftItemFactory; import org.bukkit.Material; import org.bukkit.WorldType;