diff --git a/ProtocolLib/.project b/ProtocolLib/.project index d36fcbe8..a1960c9e 100644 --- a/ProtocolLib/.project +++ b/ProtocolLib/.project @@ -11,12 +11,12 @@ - net.sourceforge.metrics.builder + org.eclipse.m2e.core.maven2Builder - org.eclipse.m2e.core.maven2Builder + net.sourceforge.metrics.builder diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 414e2599..695153b4 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.4.3 + 2.4.5 jar Provides read/write access to the Minecraft protocol. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java index 74bb63c2..dd0655cc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java @@ -1,167 +1,168 @@ -/* - * 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]); - } -} +/* + * 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()) + ); + e.printStackTrace(); + } + } + } + } + + // 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/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index 392b1efc..09be6900 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -220,14 +220,15 @@ public class CommandFilter extends CommandBase { // Script engine private ScriptEngine engine; + private boolean uninitialized; 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(); + // Tell the filter system to initialize the script first chance it gets + this.uninitialized = true; } private void initalizeScript() { @@ -238,6 +239,8 @@ public class CommandFilter extends CommandBase { // Oh for .. if (!isInitialized()) { throw new ScriptException("A JavaScript engine could not be found."); + } else { + plugin.getLogger().info("Loaded command filter engine."); } } catch (ScriptException e1) { // It's not a huge deal @@ -342,12 +345,25 @@ public class CommandFilter extends CommandBase { return true; } + /** + * Initialize the script engine if necessary. + */ + private void checkScriptStatus() { + // Start the engine + if (uninitialized) { + uninitialized = false; + initalizeScript(); + } + } + /* * Description: Adds or removes a simple packet filter. Usage: / add|remove name [packet IDs] */ @Override protected boolean handleCommand(CommandSender sender, String[] args) { + checkScriptStatus(); + if (!config.isDebug()) { sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!"); return true; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java index 8b543e32..b4337dca 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java @@ -85,6 +85,12 @@ public final class Packets { public static final int ENTITY_HEAD_ROTATION = 35; public static final int ENTITY_STATUS = 38; public static final int ATTACH_ENTITY = 39; + + /** + * Sent when an entities DataWatcher is updated. + *

+ * Remember to clone the packet if you are modifying it. + */ public static final int ENTITY_METADATA = 40; public static final int MOB_EFFECT = 41; public static final int REMOVE_MOB_EFFECT = 42; @@ -109,6 +115,12 @@ public final class Packets { public static final int SET_CREATIVE_SLOT = 107; public static final int UPDATE_SIGN = 130; public static final int ITEM_DATA = 131; + + /** + * Sent the first time a tile entity (chest inventory, etc.) is withing range of the player, or has been updated. + *

+ * Remember to clone the packet if you are modifying it. + */ public static final int TILE_ENTITY_DATA = 132; public static final int STATISTIC = 200; public static final int PLAYER_INFO = 201; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index 269bcb04..dc4c24fc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -19,6 +19,7 @@ package com.comphenix.protocol; import java.io.File; import java.io.IOException; +import java.util.List; import org.bukkit.configuration.Configuration; import org.bukkit.configuration.ConfigurationSection; @@ -26,6 +27,8 @@ import org.bukkit.plugin.Plugin; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.io.Files; /** @@ -48,6 +51,7 @@ class ProtocolConfig { private static final String INJECTION_METHOD = "injection method"; private static final String SCRIPT_ENGINE_NAME = "script engine"; + private static final String SUPPRESSED_REPORTS = "suppressed reports"; private static final String UPDATER_NOTIFY = "notify"; private static final String UPDATER_DOWNLAD = "download"; @@ -68,6 +72,9 @@ class ProtocolConfig { private boolean configChanged; private boolean valuesChanged; + // Modifications + private int modCount; + public ProtocolConfig(Plugin plugin) { this(plugin, plugin.getConfig()); } @@ -84,6 +91,7 @@ class ProtocolConfig { // Reset configChanged = false; valuesChanged = false; + modCount++; this.config = plugin.getConfig(); this.lastUpdateTime = loadLastUpdate(); @@ -199,6 +207,7 @@ class ProtocolConfig { */ public void setAutoNotify(boolean value) { setConfig(updater, UPDATER_NOTIFY, value); + modCount++; } /** @@ -215,6 +224,7 @@ class ProtocolConfig { */ public void setAutoDownload(boolean value) { setConfig(updater, UPDATER_DOWNLAD, value); + modCount++; } /** @@ -233,6 +243,24 @@ class ProtocolConfig { */ public void setDebug(boolean value) { setConfig(global, DEBUG_MODE_ENABLED, value); + modCount++; + } + + /** + * Retrieve an immutable list of every suppressed report type. + * @return Every suppressed report type. + */ + public ImmutableList getSuppressedReports() { + return ImmutableList.copyOf(global.getStringList(SUPPRESSED_REPORTS)); + } + + /** + * Set the list of suppressed report types, + * @param reports - suppressed report types. + */ + public void setSuppressedReports(List reports) { + global.set(SUPPRESSED_REPORTS, Lists.newArrayList(reports)); + modCount++; } /** @@ -255,6 +283,7 @@ class ProtocolConfig { if (delaySeconds < DEFAULT_UPDATER_DELAY) delaySeconds = DEFAULT_UPDATER_DELAY; setConfig(updater, UPDATER_DELAY, delaySeconds); + modCount++; } /** @@ -275,6 +304,7 @@ class ProtocolConfig { */ public void setIgnoreVersionCheck(String ignoreVersion) { setConfig(global, IGNORE_VERSION_CHECK, ignoreVersion); + modCount++; } /** @@ -294,6 +324,7 @@ class ProtocolConfig { */ public void setMetricsEnabled(boolean enabled) { setConfig(global, METRICS_ENABLED, enabled); + modCount++; } /** @@ -313,6 +344,7 @@ class ProtocolConfig { */ public void setBackgroundCompilerEnabled(boolean enabled) { setConfig(global, BACKGROUND_COMPILER_ENABLED, enabled); + modCount++; } /** @@ -325,6 +357,9 @@ class ProtocolConfig { /** * Set the last time we updated, in seconds since 1970.01.01 00:00. + *

+ * Note that this is not considered to modify the configuration, so the modification count + * will not be incremented. * @param lastTimeSeconds - new last update time. */ public void setAutoLastTime(long lastTimeSeconds) { @@ -348,6 +383,7 @@ class ProtocolConfig { */ public void setScriptEngineName(String name) { setConfig(global, SCRIPT_ENGINE_NAME, name); + modCount++; } /** @@ -380,6 +416,15 @@ class ProtocolConfig { */ public void setInjectionMethod(PlayerInjectHooks hook) { setConfig(global, INJECTION_METHOD, hook.name()); + modCount++; + } + + /** + * Retrieve the number of modifications made to this configuration. + * @return The number of modifications. + */ + public int getModificationCount() { + return modCount; } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index b93c662a..a9c4315f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -19,6 +19,7 @@ package com.comphenix.protocol; import java.io.File; import java.io.IOException; +import java.util.Set; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -34,6 +35,7 @@ import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.error.BasicErrorReporter; +import com.comphenix.protocol.error.DelegatedErrorReporter; import com.comphenix.protocol.error.DetailedErrorReporter; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.Report; @@ -47,6 +49,9 @@ 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; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; /** * The main entry point for ProtocolLib. @@ -136,12 +141,12 @@ public class ProtocolLibrary extends JavaPlugin { // Add global parameters DetailedErrorReporter detailedReporter = new DetailedErrorReporter(this); - reporter = detailedReporter; + reporter = getFilteredReporter(detailedReporter); try { config = new ProtocolConfig(this); } catch (Exception e) { - detailedReporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_CONFIG).error(e)); + reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_CONFIG).error(e)); // Load it again if (deleteConfig()) { @@ -168,7 +173,7 @@ public class ProtocolLibrary extends JavaPlugin { unhookTask = new DelayedSingleTask(this); protocolManager = new PacketFilterManager( - getClassLoader(), getServer(), this, version, unhookTask, detailedReporter); + getClassLoader(), getServer(), this, version, unhookTask, reporter); // Setup error reporter detailedReporter.addGlobalParameter("manager", protocolManager); @@ -183,23 +188,52 @@ public class ProtocolLibrary extends JavaPlugin { protocolManager.setPlayerHook(hook); } } catch (IllegalArgumentException e) { - detailedReporter.reportWarning(config, Report.newBuilder(REPORT_CANNOT_PARSE_INJECTION_METHOD).error(e)); + reporter.reportWarning(config, Report.newBuilder(REPORT_CANNOT_PARSE_INJECTION_METHOD).error(e)); } // Initialize command handlers - commandProtocol = new CommandProtocol(detailedReporter, this, updater, config); - commandFilter = new CommandFilter(detailedReporter, this, config); - commandPacket = new CommandPacket(detailedReporter, this, logger, commandFilter, protocolManager); + commandProtocol = new CommandProtocol(reporter, this, updater, config); + commandFilter = new CommandFilter(reporter, this, config); + commandPacket = new CommandPacket(reporter, this, logger, commandFilter, protocolManager); // Send logging information to player listeners too setupBroadcastUsers(PERMISSION_INFO); } catch (Throwable e) { - detailedReporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_LOAD_ERROR).error(e).callerParam(protocolManager)); + reporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_LOAD_ERROR).error(e).callerParam(protocolManager)); disablePlugin(); } } + /** + * Retrieve a error reporter that may be filtered by the configuration. + * @return The new default error reporter. + */ + private ErrorReporter getFilteredReporter(ErrorReporter reporter) { + return new DelegatedErrorReporter(reporter) { + private int lastModCount = -1; + private Set reports = Sets.newHashSet(); + + @Override + protected Report filterReport(Object sender, Report report, boolean detailed) { + String canonicalName = ReportType.getReportName(sender.getClass(), report.getType()); + String reportName = Iterables.getLast(Splitter.on("#").split(canonicalName)).toUpperCase(); + + if (config != null && config.getModificationCount() != lastModCount) { + // Update our cached set again + reports = Sets.newHashSet(config.getSuppressedReports()); + lastModCount = config.getModificationCount(); + } + + // Cancel reports either on the full canonical name, or just the report name + if (reports.contains(canonicalName) || reports.contains(reportName)) + return null; + else + return report; + } + }; + } + private boolean deleteConfig() { return config.getFile().delete(); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java index cd4490e0..e795677c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java @@ -1,66 +1,115 @@ -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]); - } -} +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; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; + +/** + * 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; + + // Used to store the report name + protected String reportName; + + /** + * 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 the full canonical name of a given report type. + *

+ * This is in the format canonical_name_of_class#report_type + * @param clazz - the sender class. + * @param type - the report instance. + * @return The full canonical name. + */ + public static String getReportName(Class sender, ReportType type) { + if (sender == null) + throw new IllegalArgumentException("sender cannot be NUll."); + + // Whether or not we need to retrieve the report name again + if (type.reportName == null) { + for (Field field : getReportFields(sender)) { + try { + field.setAccessible(true); + + if (field.get(null) == type) { + // We got the right field! + return type.reportName = field.getDeclaringClass().getCanonicalName() + "#" + field.getName(); + } + } catch (IllegalAccessException e) { + throw new FieldAccessException("Unable to read field " + field, e); + } + } + throw new IllegalArgumentException("Cannot find report name for " + type); + } + return type.reportName; + } + + /** + * Retrieve all publicly associated reports. + * @param sender - sender class. + * @return All associated reports. + */ + public static ReportType[] getReports(Class sender) { + if (sender == null) + throw new IllegalArgumentException("sender cannot be NULL."); + List result = new ArrayList(); + + // Go through all the fields + for (Field field : getReportFields(sender)) { + try { + field.setAccessible(true); + result.add((ReportType) field.get(null)); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Unable to read field " + field, e); + } + } + return result.toArray(new ReportType[0]); + } + + /** + * Retrieve all publicly associated report fields. + * @param clazz - sender class. + * @return All associated report fields. + */ + private static List getReportFields(Class clazz) { + return FuzzyReflection.fromClass(clazz).getFieldList( + FuzzyFieldContract.newBuilder(). + requireModifier(Modifier.STATIC). + typeDerivedOf(ReportType.class). + build() + ); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index 85cebfe7..46d5e7f2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -285,43 +285,11 @@ public class PacketContainer implements Serializable { * internal Minecraft ItemStack. * @return A modifier for ItemStack array fields. */ - public StructureModifier getItemArrayModifier() { - - final EquivalentConverter stackConverter = BukkitConverters.getItemStackConverter(); - + public StructureModifier getItemArrayModifier() { // Convert to and from the Bukkit wrapper return structureModifier.withType( MinecraftReflection.getItemStackArrayClass(), - BukkitConverters.getIgnoreNull(new EquivalentConverter() { - - public Object getGeneric(ClassgenericType, ItemStack[] specific) { - Class nmsStack = MinecraftReflection.getItemStackClass(); - Object[] result = (Object[]) Array.newInstance(nmsStack, specific.length); - - // Unwrap every item - for (int i = 0; i < result.length; i++) { - result[i] = stackConverter.getGeneric(nmsStack, specific[i]); - } - return result; - } - - @Override - public ItemStack[] getSpecific(Object generic) { - Object[] input = (Object[]) generic; - ItemStack[] result = new ItemStack[input.length]; - - // Add the wrapper - for (int i = 0; i < result.length; i++) { - result[i] = stackConverter.getSpecific(input[i]); - } - return result; - } - - @Override - public Class getSpecificType() { - return ItemStack[].class; - } - })); + BukkitConverters.getIgnoreNull(new ItemStackArrayConverter())); } /** @@ -556,4 +524,40 @@ public class PacketContainer implements Serializable { return method; } + + /** + * Represents an equivalent converter for ItemStack arrays. + * @author Kristian + */ + private static class ItemStackArrayConverter implements EquivalentConverter { + final EquivalentConverter stackConverter = BukkitConverters.getItemStackConverter(); + + public Object getGeneric(ClassgenericType, ItemStack[] specific) { + Class nmsStack = MinecraftReflection.getItemStackClass(); + Object[] result = (Object[]) Array.newInstance(nmsStack, specific.length); + + // Unwrap every item + for (int i = 0; i < result.length; i++) { + result[i] = stackConverter.getGeneric(nmsStack, specific[i]); + } + return result; + } + + @Override + public ItemStack[] getSpecific(Object generic) { + Object[] input = (Object[]) generic; + ItemStack[] result = new ItemStack[input.length]; + + // Add the wrapper + for (int i = 0; i < result.length; i++) { + result[i] = stackConverter.getSpecific(input[i]); + } + return result; + } + + @Override + public Class getSpecificType() { + return ItemStack[].class; + } + } } 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 c9a0b6bb..a3fc2748 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,352 +1,358 @@ -/* - * 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; - } -} +/* + * 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) { + callbackFilter = new SendMethodFilter(); + } + + 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; + } + } + + 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; + } + + /** + * Represents a CallbackFilter that only matches the SendPacket method. + * @author Kristian + */ + private static class SendMethodFilter implements CallbackFilter { + private Method sendPacket = MinecraftMethods.getSendPacketMethod(); + + @Override + public int accept(Method method) { + if (isCallableEqual(sendPacket, method)) { + NetworkServerInjector.foundSendPacket = true; + return 0; + } else { + return 1; + } + } + + /** + * 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()); + } + } +} 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 02036a07..bf5a0cd9 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,664 +1,686 @@ -/* - * 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; - } -} +/* + * 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."); + + public static final ReportType REPORT_INVALID_NETWORK_MANAGER = new ReportType("NetworkManager doesn't appear to be valid."); + + // 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) { + // Inform about the state of the network manager too + reporter.reportWarning( + this, Report.newBuilder(REPORT_INVALID_NETWORK_MANAGER).callerParam(networkManager).build()); + 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 currentHandler = FieldUtils.readField(serverHandlerField, notchEntity, true); + + // This is bad + if (currentHandler == null) + throw new IllegalAccessError("Unable to fetch server handler: was NUll."); + + // See if this isn't a standard net handler class + if (!isStandardMinecraftNetHandler(currentHandler)) { + // This is our proxy object + if (currentHandler 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(currentHandler, 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; + } + + /** + * Determine if a given object is a standard Minecraft net handler. + * @param obj the object to test. + * @return TRUE if it is, FALSE otherwise. + */ + private boolean isStandardMinecraftNetHandler(Object obj) { + if (obj == null) + return false; + Class clazz = obj.getClass(); + + return MinecraftReflection.getNetLoginHandlerClass().equals(clazz) || + MinecraftReflection.getNetServerHandlerClass().equals(clazz); + } + + /** + * Retrieves the current net handler for this player. + * @return Current net handler. + * @throws IllegalAccessException Unable to find or retrieve net handler. + */ + protected Object getNetHandler() throws IllegalAccessException { + + // What a mess + try { + if (netHandlerField == null) + netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). + getFieldByType("NetHandler", MinecraftReflection.getNetHandlerClass()); + } catch (RuntimeException e1) { + // Swallow it + } + + // Second attempt + if (netHandlerField == null) { + try { + // Well, that sucks. Try just Minecraft objects then. + netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). + getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); + + } catch (RuntimeException e2) { + throw new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); + } + } + + // Get the handler + if (netHandler == null) + netHandler = FieldUtils.readField(netHandlerField, networkManager, true); + return netHandler; + } + + /** + * Retrieve the stored entity player from a given NetHandler. + * @param netHandler - the nethandler to retrieve it from. + * @return The stored entity player. + * @throws IllegalAccessException If the reflection failed. + */ + private Object getEntityPlayer(Object netHandler) throws IllegalAccessException { + if (entityPlayerField == null) + entityPlayerField = FuzzyReflection.fromObject(netHandler).getFieldByType( + "EntityPlayer", MinecraftReflection.getEntityPlayerClass()); + return FieldUtils.readField(entityPlayerField, netHandler); + } + + /** + * Processes the given packet as if it was transmitted by the current player. + * @param packet - packet to process. + * @throws IllegalAccessException If the reflection machinery failed. + * @throws InvocationTargetException If the underlying method caused an error. + */ + public void processPacket(Object packet) throws IllegalAccessException, InvocationTargetException { + + Object netHandler = getNetHandler(); + + // Get the process method + if (processMethod == null) { + try { + processMethod = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()). + getMethodByParameters("processPacket", netHandlerField.getType()); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Cannot locate process packet method: " + e.getMessage()); + } + } + + // We're ready + try { + processMethod.invoke(packet, netHandler); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Method " + processMethod.getName() + " is not compatible."); + } catch (InvocationTargetException e) { + throw e; + } + } + + /** + * Send a packet to the client. + * @param packet - server packet to send. + * @param filtered - whether or not the packet will be filtered by our listeners. + * @param InvocationTargetException If an error occured when sending the packet. + */ + @Override + public abstract void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException; + + /** + * Inject a hook to catch packets sent to the current player. + */ + public abstract void injectManager(); + + /** + * Remove all hooks and modifications. + */ + public final void cleanupAll() { + if (!clean) + cleanHook(); + clean = true; + } + + /** + * Clean up after the player has disconnected. + */ + public abstract void handleDisconnect(); + + /** + * Override to add custom cleanup behavior. + */ + protected abstract void cleanHook(); + + /** + * Determine whether or not this hook has already been cleaned. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean isClean() { + return clean; + } + + /** + * Determine if this inject method can even be attempted. + * @return TRUE if can be attempted, though possibly with failure, FALSE otherwise. + */ + public abstract boolean canInject(GamePhase state); + + /** + * Retrieve the hook type this class represents. + * @return Hook type this class represents. + */ + public abstract PlayerInjectHooks getHookType(); + + /** + * Invoked before a new listener is registered. + *

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

+ * Should only be called during the creation of the injector. + * @param player - the new hooked player. + */ + public void setPlayer(Player player) { + this.player = player; + } + + /** + * Object that can invoke the packet events. + * @return Packet event invoker. + */ + public ListenerInvoker getInvoker() { + return invoker; + } + + /** + * Retrieve the hooked player object OR the more up-to-date player instance. + * @return The hooked player, or a more up-to-date instance. + */ + @Override + public Player getUpdatedPlayer() { + if (updatedPlayer != null) + return updatedPlayer; + else + return player; + } + + @Override + public void transferState(SocketInjector delegate) { + // Do nothing + } + + @Override + public void setUpdatedPlayer(Player updatedPlayer) { + this.updatedPlayer = updatedPlayer; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java index 99f6c550..1f3fa663 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,710 +1,733 @@ -/* - * 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; - } -} +/* + * 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.ref.WeakReference; +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 WeakReference 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(); + + // Ignore logged out players + if (address == null) + return null; + + 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 = new WeakReference(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) { + SocketAddress address = player.getAddress(); + + // Ignore logged out players + if (address != null) { + SocketInjector injector = inputStreamLookup.peekSocketInjector(address); + + 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 (getLastSuccessfulHook() != null) { + for (PacketListener listener : listeners) { + checkListener(listener); + } + } + } + + /** + * Retrieve the last successful hook. + *

+ * May be NULL if the hook has been uninjected. + * @return Last successful hook. + */ + private PlayerInjector getLastSuccessfulHook() { + return lastSuccessfulHook != null ? lastSuccessfulHook.get() : null; + } + + /** + * 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) { + PlayerInjector last = getLastSuccessfulHook(); + + if (last != null) { + UnsupportedListener result = last.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/spigot/SpigotPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java index 555a5304..070e3c14 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 @@ -5,7 +5,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.bukkit.Bukkit; @@ -37,6 +36,7 @@ import com.comphenix.protocol.reflect.MethodInfo; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.utility.MinecraftReflection; import com.google.common.collect.MapMaker; +import com.google.common.collect.Maps; /** * Offload all the work to Spigot, if possible. @@ -58,6 +58,42 @@ public class SpigotPacketInjector implements SpigotPacketListener { * The amount of ticks to wait before removing all traces of a player. */ private static final int CLEANUP_DELAY = 100; + + // The listener we will register on Spigot. + // Unfortunately, due to the use of PlayerConnection, INetworkManager and Packet, we're + // unable to reference it directly. But with CGLib, it shouldn't cost us much. + private Object dynamicListener; + + // Reference to ProtocolLib + private Plugin plugin; + + // Different sending filters + private IntegerSet queuedFilters; + private IntegerSet reveivedFilters; + + // NetworkManager to injector and player + private ConcurrentMap networkManagerInjector = Maps.newConcurrentMap(); + + // Player to injector + private ConcurrentMap playerInjector = Maps.newConcurrentMap(); + + // Responsible for informing the PL packet listeners + private ListenerInvoker invoker; + private ErrorReporter reporter; + private Server server; + private ClassLoader classLoader; + + /** + * Create a new spigot injector. + */ + public SpigotPacketInjector(ClassLoader classLoader, ErrorReporter reporter, ListenerInvoker invoker, Server server) { + this.classLoader = classLoader; + this.reporter = reporter; + this.invoker = invoker; + this.server = server; + this.queuedFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + this.reveivedFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + } /** * Retrieve the spigot packet listener class. @@ -107,42 +143,6 @@ public class SpigotPacketInjector implements SpigotPacketListener { return getSpigotListenerClass() != null; } - // The listener we will register on Spigot. - // Unfortunately, due to the use of PlayerConnection, INetworkManager and Packet, we're - // unable to reference it directly. But with CGLib, it shouldn't cost us much. - private Object dynamicListener; - - // Reference to ProtocolLib - private Plugin plugin; - - // Different sending filters - private IntegerSet queuedFilters; - private IntegerSet reveivedFilters; - - // NetworkManager to injector and player - private ConcurrentMap networkManagerInjector = new ConcurrentHashMap(); - - // Player to injector - private ConcurrentMap playerInjector = new ConcurrentHashMap(); - - // Responsible for informing the PL packet listeners - private ListenerInvoker invoker; - private ErrorReporter reporter; - private Server server; - private ClassLoader classLoader; - - /** - * Create a new spigot injector. - */ - public SpigotPacketInjector(ClassLoader classLoader, ErrorReporter reporter, ListenerInvoker invoker, Server server) { - this.classLoader = classLoader; - this.reporter = reporter; - this.invoker = invoker; - this.server = server; - this.queuedFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); - this.reveivedFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); - } - /** * Register the Spigot packet injector. * @param plugin - the parent plugin. @@ -455,7 +455,7 @@ public class SpigotPacketInjector implements SpigotPacketListener { // Clean up playerInjector.remove(injector.getPlayer()); playerInjector.remove(injector.getUpdatedPlayer()); - networkManagerInjector.remove(injector); + networkManagerInjector.remove(injector.getNetworkManager()); } }, CLEANUP_DELAY); } diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index 02c7dd23..2ca7f9ef 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -22,4 +22,6 @@ global: debug: false # The engine used by the filter command - script engine: JavaScript \ No newline at end of file + script engine: JavaScript + + suppressed reports: \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 423ece96..010f513f 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.4.3 +version: 2.4.5 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib