From a798147e71958bc2c638c2188bcad8d6959d5de3 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 10 Mar 2013 18:52:41 +0100 Subject: [PATCH 01/46] Correctly override SpoutPlugin's sendPacket method. --- ProtocolLib/pom.xml | 24 +++++++------- .../player/NetworkServerInjector.java | 32 +++++++++++++++---- ProtocolLib/src/main/resources/plugin.yml | 2 +- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index ac7a8b99..586f9634 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.3.0 + 2.3.1-SNAPSHOT jar Provides read/write access to the Minecraft protocol. @@ -219,16 +219,16 @@ test - org.powermock - powermock-module-junit4 - ${powermock.version} - test - - - org.powermock - powermock-api-mockito - ${powermock.version} - test - + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java index 16218066..ef6858bc 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 @@ -20,6 +20,8 @@ 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; @@ -45,8 +47,8 @@ import com.comphenix.protocol.utility.MinecraftReflection; * @author Kristian */ class NetworkServerInjector extends PlayerInjector { - private volatile static CallbackFilter callbackFilter; + private volatile static boolean foundSendPacket; private volatile static Field disconnectField; private InjectedServerConnection serverInjection; @@ -168,10 +170,12 @@ class NetworkServerInjector extends PlayerInjector { callbackFilter = new CallbackFilter() { @Override public int accept(Method method) { - if (method.equals(sendPacket)) + if (isCallableEqual(sendPacket, method)) { + foundSendPacket = true; return 0; - else + } else { return 1; + } } }; } @@ -204,9 +208,11 @@ class NetworkServerInjector extends PlayerInjector { // Inject it now if (proxyObject != null) { - // This will be done by InjectedServerConnection instead - //copyTo(serverHandler, proxyObject); - + // 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; @@ -215,6 +221,20 @@ class NetworkServerInjector extends PlayerInjector { } } + /** + * 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 { diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 23fb34bf..c95bbdcf 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.3.0 +version: 2.3.1-SNAPSHOT description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib From 3c97cffc09f2c0179d803b426fc0e75e5fca8783 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 12 Mar 2013 00:52:09 +0100 Subject: [PATCH 02/46] After Minecraft 1.4.4, CraftBukkit no longer redirects MAP_CHUNK. We can therefore relax the requirements in NetworkFieldInjector and NetworkObjectInjetor. --- .../comphenix/protocol/ProtocolLibrary.java | 18 ++++++++----- .../injector/PacketFilterManager.java | 12 +++++++-- .../injector/player/NetworkFieldInjector.java | 27 +++++++++++++------ .../player/NetworkObjectInjector.java | 26 +++++++++++++----- .../player/NetworkServerInjector.java | 3 ++- .../player/PlayerInjectionHandler.java | 4 ++- .../injector/player/PlayerInjector.java | 5 +++- .../player/PlayerInjectorBuilder.java | 14 +++++++++- .../player/ProxyPlayerInjectionHandler.java | 13 ++++++--- .../injector/spigot/DummyPlayerHandler.java | 2 +- .../{ => utility}/MinecraftVersion.java | 4 +-- .../protocol/MinecraftVersionTest.java | 2 ++ 12 files changed, 96 insertions(+), 34 deletions(-) rename ProtocolLib/src/main/java/com/comphenix/protocol/{ => utility}/MinecraftVersion.java (98%) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 271c1181..68fc0808 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -43,6 +43,7 @@ import com.comphenix.protocol.metrics.Updater; import com.comphenix.protocol.metrics.Updater.UpdateResult; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; import com.comphenix.protocol.utility.ChatExtensions; +import com.comphenix.protocol.utility.MinecraftVersion; /** * The main entry point for ProtocolLib. @@ -132,12 +133,15 @@ public class ProtocolLibrary extends JavaPlugin { // Check for other versions checkConflictingVersions(); + // Handle unexpected Minecraft versions + MinecraftVersion version = verifyMinecraftVersion(); + // Set updater updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info"); unhookTask = new DelayedSingleTask(this); protocolManager = new PacketFilterManager( - getClassLoader(), getServer(), unhookTask, detailedReporter); + getClassLoader(), getServer(), version, unhookTask, detailedReporter); // Setup error reporter detailedReporter.addGlobalParameter("manager", protocolManager); @@ -248,10 +252,7 @@ public class ProtocolLibrary extends JavaPlugin { } else { logger.info("Structure compiler thread has been disabled."); } - - // Handle unexpected Minecraft versions - verifyMinecraftVersion(); - + // Set up command handlers registerCommand(CommandProtocol.NAME, commandProtocol); registerCommand(CommandPacket.NAME, commandPacket); @@ -282,7 +283,7 @@ public class ProtocolLibrary extends JavaPlugin { } // Used to check Minecraft version - private void verifyMinecraftVersion() { + private MinecraftVersion verifyMinecraftVersion() { try { MinecraftVersion minimum = new MinecraftVersion(MINIMUM_MINECRAFT_VERSION); MinecraftVersion maximum = new MinecraftVersion(MAXIMUM_MINECRAFT_VERSION); @@ -296,9 +297,14 @@ public class ProtocolLibrary extends JavaPlugin { if (current.compareTo(maximum) > 0) logger.warning("Version " + current + " has not yet been tested! Proceed with caution."); } + return current; + } catch (Exception e) { reporter.reportWarning(this, "Unable to retrieve current Minecraft version.", e); } + + // Unknown version + return null; } private void checkConflictingVersions() { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index c5b8c0ac..0bdb1023 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -61,6 +61,7 @@ import com.comphenix.protocol.injector.spigot.SpigotPacketInjector; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableSet; @@ -149,9 +150,15 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok /** * Only create instances of this class if protocol lib is disabled. - * @param unhookTask */ public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) { + this(classLoader, server, new MinecraftVersion(server), unhookTask, reporter); + } + + /** + * Only create instances of this class if protocol lib is disabled. + */ + public PacketFilterManager(ClassLoader classLoader, Server server, MinecraftVersion mcVersion, DelayedSingleTask unhookTask, ErrorReporter reporter) { if (reporter == null) throw new IllegalArgumentException("reporter cannot be NULL."); if (classLoader == null) @@ -201,6 +208,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok classLoader(classLoader). packetListeners(packetListeners). injectionFilter(isInjectionNecessary). + version(mcVersion). buildHandler(); this.packetInjector = PacketInjectorBuilder.newBuilder(). @@ -561,7 +569,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok return; } - playerInjection.processPacket(sender, mcPacket); + playerInjection.recieveClientPacket(sender, mcPacket); } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java index 88d9beec..559c93ef 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java @@ -39,6 +39,7 @@ import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.VolatileField; +import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.collect.Sets; /** @@ -56,6 +57,12 @@ class NetworkFieldInjector extends PlayerInjector { // Nothing } + // After commit 336a4e00668fd2518c41242755ed6b3bdc3b0e6c (Update CraftBukkit to Minecraft 1.4.4.), + // CraftBukkit stopped redirecting map chunk and map chunk bulk packets to a separate queue. + // Thus, NetworkFieldInjector can safely handle every packet (though not perfectly - some packets + // will be slightly processed). + private MinecraftVersion safeVersion = new MinecraftVersion("1.4.4"); + // Packets to ignore private Set ignoredPackets = Sets.newSetFromMap(new ConcurrentHashMap()); @@ -99,7 +106,6 @@ class NetworkFieldInjector extends PlayerInjector { @Override public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException { - if (networkManager != null) { try { if (!filtered) { @@ -122,14 +128,19 @@ class NetworkFieldInjector extends PlayerInjector { } @Override - public UnsupportedListener checkListener(PacketListener listener) { - int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; - - // Unfortunately, we don't support chunk packets - if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { - return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported); - } else { + public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { + if (version != null && version.compareTo(safeVersion) > 0) { return null; + + } else { + int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; + + // Unfortunately, we don't support chunk packets + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { + return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported); + } else { + return null; + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java index 69acf343..2ea0da49 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java @@ -40,6 +40,7 @@ import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.utility.MinecraftVersion; /** * Injection method that overrides the NetworkHandler itself, and its queue-method. @@ -53,6 +54,12 @@ public class NetworkObjectInjector extends PlayerInjector { // Used to construct proxy objects private ClassLoader classLoader; + // After commit 336a4e00668fd2518c41242755ed6b3bdc3b0e6c (Update CraftBukkit to Minecraft 1.4.4.), + // CraftBukkit stopped redirecting map chunk and map chunk bulk packets to a separate queue. + // Thus, NetworkFieldInjector can safely handle every packet (though not perfectly - some packets + // will be slightly processed). + private MinecraftVersion safeVersion = new MinecraftVersion("1.4.4"); + // Shared callback filter - avoid creating a new class every time private volatile static CallbackFilter callbackFilter; @@ -117,14 +124,19 @@ public class NetworkObjectInjector extends PlayerInjector { } @Override - public UnsupportedListener checkListener(PacketListener listener) { - int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; - - // Unfortunately, we don't support chunk packets - if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { - return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported); - } else { + public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { + if (version != null && version.compareTo(safeVersion) > 0) { return null; + + } else { + int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; + + // Unfortunately, we don't support chunk packets + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { + return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported); + } else { + return null; + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java index ef6858bc..207bcba3 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 @@ -40,6 +40,7 @@ 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. @@ -326,7 +327,7 @@ class NetworkServerInjector extends PlayerInjector { } @Override - public UnsupportedListener checkListener(PacketListener listener) { + public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) { // We support everything return null; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java index 8c59c15a..b26769fa 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java @@ -126,11 +126,12 @@ public interface PlayerInjectionHandler { * @throws IllegalAccessException If the reflection machinery failed. * @throws InvocationTargetException If the underlying method caused an error. */ - public abstract void processPacket(Player player, Object mcPacket) + public abstract void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException; /** * Determine if the given listeners are valid. + * @param version - the current Minecraft version, or NULL if unknown. * @param listeners - listeners to check. */ public abstract void checkListener(Set listeners); @@ -139,6 +140,7 @@ public interface PlayerInjectionHandler { * Determine if a listener is valid or not. *

* If not, a warning will be printed to the console. + * @param version - the current Minecraft version, or NULL if unknown. * @param listener - listener to check. */ public abstract void checkListener(PacketListener listener); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java index 524bd40a..a2fa3362 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 @@ -43,6 +43,7 @@ import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.VolatileField; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; abstract class PlayerInjector implements SocketInjector { @@ -508,11 +509,13 @@ abstract class PlayerInjector implements SocketInjector { * 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(PacketListener listener); + public abstract UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener); /** * Allows a packet to be sent by the listeners. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java index 120d5dc9..77ccbf5f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java @@ -14,6 +14,7 @@ import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager; +import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; @@ -37,6 +38,7 @@ public class PlayerInjectorBuilder { protected ListenerInvoker invoker; protected Set packetListeners; protected Server server; + protected MinecraftVersion version; /** * Set the class loader to use during class generation. @@ -107,6 +109,16 @@ public class PlayerInjectorBuilder { return this; } + /** + * Set the current Minecraft version. + * @param server - the current Minecraft version, or NULL if unknown. + * @return This builder, for chaining. + */ + public PlayerInjectorBuilder version(MinecraftVersion version) { + this.version = version; + return this; + } + /** * Called before an object is created with this builder. */ @@ -140,6 +152,6 @@ public class PlayerInjectorBuilder { return new ProxyPlayerInjectionHandler( classLoader, reporter, injectionFilter, - invoker, packetListeners, server); + invoker, packetListeners, server, version); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java index 35020261..5a9be055 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 @@ -45,6 +45,7 @@ import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; import com.comphenix.protocol.injector.server.InputStreamLookupBuilder; import com.comphenix.protocol.injector.server.SocketInjector; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Predicate; import com.google.common.cache.Cache; @@ -91,6 +92,9 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { // 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); @@ -105,13 +109,14 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { public ProxyPlayerInjectionHandler( ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, - ListenerInvoker invoker, Set packetListeners, Server server) { + 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). @@ -501,14 +506,14 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { } /** - * Process a packet as if it were sent by the given player. + * 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 processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { PlayerInjector injector = getInjector(player); // Process the given packet, or simply give up @@ -619,7 +624,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { @Override public void checkListener(PacketListener listener) { if (lastSuccessfulHook != null) { - UnsupportedListener result = lastSuccessfulHook.checkListener(listener); + UnsupportedListener result = lastSuccessfulHook.checkListener(version, listener); // We won't prevent the listener, as it may still have valid packets if (result != null) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java index dc6f5e9d..08b1a8d2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java @@ -74,7 +74,7 @@ class DummyPlayerHandler implements PlayerInjectionHandler { } @Override - public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { injector.processPacket(player, mcPacket); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/MinecraftVersion.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java similarity index 98% rename from ProtocolLib/src/main/java/com/comphenix/protocol/MinecraftVersion.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java index 53fb9898..e78dc266 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/MinecraftVersion.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol; +package com.comphenix.protocol.utility; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,7 +31,7 @@ import com.google.common.collect.Ordering; * * @author Kristian */ -class MinecraftVersion implements Comparable { +public class MinecraftVersion implements Comparable { /** * Regular expression used to parse version strings. */ diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java index 8f98de70..2d127c23 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java @@ -21,6 +21,8 @@ import static org.junit.Assert.*; import org.junit.Test; +import com.comphenix.protocol.utility.MinecraftVersion; + public class MinecraftVersionTest { @Test From 1bd0db20ce521a128d85d1b03f8faa2ecd420041 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 12 Mar 2013 01:16:07 +0100 Subject: [PATCH 03/46] Invoke MONITOR listeners, even if we are bypassing normal listeners. --- .../com/comphenix/protocol/PacketStream.java | 5 +- .../comphenix/protocol/ProtocolManager.java | 5 +- .../AbstractConcurrentListenerMultimap.java | 4 -- .../injector/PacketFilterManager.java | 15 +++++ .../injector/SortedPacketListenerList.java | 55 ++++++++++++++++++- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java b/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java index 6d07232c..a243452a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java @@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException; import org.bukkit.entity.Player; +import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketContainer; /** @@ -43,7 +44,7 @@ public interface PacketStream { * Send a packet to the given player. * @param reciever - the reciever. * @param packet - packet to send. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException - if an error occured when sending the packet. */ public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) @@ -63,7 +64,7 @@ public interface PacketStream { * Simulate recieving a certain packet from a given player. * @param sender - the sender. * @param packet - the packet that was sent. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException If the reflection machinery failed. * @throws IllegalAccessException If the underlying method caused an error. */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java index 741c54bc..a9da948f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java @@ -27,6 +27,7 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.async.AsyncMarker; +import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.PacketConstructor; @@ -47,7 +48,7 @@ public interface ProtocolManager extends PacketStream { * * @param reciever - the reciever. * @param packet - packet to send. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException - if an error occured when sending the packet. */ @Override @@ -62,7 +63,7 @@ public interface ProtocolManager extends PacketStream { * * @param sender - the sender. * @param packet - the packet that was sent. - * @param filters - whether or not to invoke any packet filters. + * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}. * @throws InvocationTargetException If the reflection machinery failed. * @throws IllegalAccessException If the underlying method caused an error. */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java index 52d869db..a3fdefac 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java @@ -34,7 +34,6 @@ import com.google.common.collect.Iterables; * @author Kristian */ public abstract class AbstractConcurrentListenerMultimap { - // The core of our map private ConcurrentMap>> listeners = new ConcurrentHashMap>>(); @@ -45,7 +44,6 @@ public abstract class AbstractConcurrentListenerMultimap { * @param whitelist - the packet whitelist to use. */ public void addListener(TListener listener, ListeningWhitelist whitelist) { - PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority()); for (Integer packetID : whitelist.getWhitelist()) { @@ -55,7 +53,6 @@ public abstract class AbstractConcurrentListenerMultimap { // Add the listener to a specific packet notifcation list private void addListener(Integer packetID, PrioritizedListener listener) { - SortedCopyOnWriteArray> list = listeners.get(packetID); // We don't want to create this for every lookup @@ -84,7 +81,6 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Every packet ID that was removed due to no listeners. */ public List removeListener(TListener listener, ListeningWhitelist whitelist) { - List removedPackets = new ArrayList(); // Again, not terribly efficient. But adding or removing listeners should be a rare event. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 0bdb1023..28b79198 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -537,6 +537,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (packetCreation.compareAndSet(false, true)) incrementPhases(GamePhase.PLAYING); + // Inform the MONITOR packets + if (!filters) { + sendingListeners.invokePacketSending( + reporter, + PacketEvent.fromServer(this, packet, reciever), + ListenerPriority.MONITOR); + } + playerInjection.sendServerPacket(reciever, packet, filters); } @@ -567,6 +575,13 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok mcPacket = event.getPacket().getHandle(); else return; + + } else { + // Let the monitors know though + recievedListeners.invokePacketSending( + reporter, + PacketEvent.fromClient(this, packet, sender), + ListenerPriority.MONITOR); } playerInjection.recieveClientPacket(sender, mcPacket); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java index 9c895e28..9862b3ca 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -21,6 +21,7 @@ import java.util.Collection; import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; @@ -30,7 +31,6 @@ import com.comphenix.protocol.events.PacketListener; * @author Kristian */ public final class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { - /** * Invokes the given packet event for every registered listener. * @param reporter - the error reporter that will be used to inform about listener exceptions. @@ -54,6 +54,33 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu } } + /** + * Invokes the given packet event for every registered listener of the given priority. + * @param reporter - the error reporter that will be used to inform about listener exceptions. + * @param event - the packet event to invoke. + * @param priorityFilter - the required priority for a listener to be invoked. + */ + public void invokePacketRecieving(ErrorReporter reporter, PacketEvent event, ListenerPriority priorityFilter) { + Collection> list = getListener(event.getPacketID()); + + if (list == null) + return; + + for (PrioritizedListener element : list) { + final PacketListener listener = element.getListener(); + + try { + if (listener.getReceivingWhitelist().getPriority() == priorityFilter) { + listener.onPacketReceiving(event); + } + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + reporter.reportMinimal(listener.getPlugin(), "onPacketReceiving(PacketEvent)", e, + event.getPacket().getHandle()); + } + } + } + /** * Invokes the given packet event for every registered listener. * @param reporter - the error reporter that will be used to inform about listener exceptions. @@ -76,4 +103,30 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu } } + /** + * Invokes the given packet event for every registered listener of the given priority. + * @param reporter - the error reporter that will be used to inform about listener exceptions. + * @param event - the packet event to invoke. + * @param priorityFilter - the required priority for a listener to be invoked. + */ + public void invokePacketSending(ErrorReporter reporter, PacketEvent event, ListenerPriority priorityFilter) { + Collection> list = getListener(event.getPacketID()); + + if (list == null) + return; + + for (PrioritizedListener element : list) { + final PacketListener listener = element.getListener(); + + try { + if (listener.getSendingWhitelist().getPriority() == priorityFilter) { + listener.onPacketSending(event); + } + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + reporter.reportMinimal(listener.getPlugin(), "onPacketSending(PacketEvent)", e, + event.getPacket().getHandle()); + } + } + } } From e3cfa45607d2bf4a8ad4a4d684a6889ef15b586a Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 12 Mar 2013 02:02:36 +0100 Subject: [PATCH 04/46] Ensure that monitor listeners cannot modify a packet event. --- .../protocol/events/PacketEvent.java | 34 ++++++++++++++++++- .../injector/SortedPacketListenerList.java | 20 +++++------ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java index c35330cb..5d43ff50 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java @@ -44,6 +44,9 @@ public class PacketEvent extends EventObject implements Cancellable { private AsyncMarker asyncMarker; private boolean asynchronous; + // Whether or not a packet event is read only + private boolean readOnly; + /** * Use the static constructors to create instances of this event. * @param source - the event source. @@ -114,6 +117,8 @@ public class PacketEvent extends EventObject implements Cancellable { * @param packet - the packet that will be sent instead. */ public void setPacket(PacketContainer packet) { + if (readOnly) + throw new IllegalStateException("The packet event is read-only."); this.packet = packet; } @@ -147,6 +152,8 @@ public class PacketEvent extends EventObject implements Cancellable { * @param cancel - TRUE if it should be cancelled, FALSE otherwise. */ public void setCancelled(boolean cancel) { + if (readOnly) + throw new IllegalStateException("The packet event is read-only."); this.cancel = cancel; } @@ -193,9 +200,34 @@ public class PacketEvent extends EventObject implements Cancellable { public void setAsyncMarker(AsyncMarker asyncMarker) { if (isAsynchronous()) throw new IllegalStateException("The marker is immutable for asynchronous events"); + if (readOnly) + throw new IllegalStateException("The packet event is read-only."); this.asyncMarker = asyncMarker; } + /** + * Determine if the current packet event is read only. + *

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

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

+ * This will be reset for every packet listener. + * @param readOnly - TRUE if it is read-only, FALSE otherwise. + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + /** * Determine if the packet event has been executed asynchronously or not. * @return TRUE if this packet event is asynchronous, FALSE otherwise. @@ -203,7 +235,7 @@ public class PacketEvent extends EventObject implements Cancellable { public boolean isAsynchronous() { return asynchronous; } - + private void writeObject(ObjectOutputStream output) throws IOException { // Default serialization output.defaultWriteObject(); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java index 9862b3ca..53fe6366 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -45,6 +45,7 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu // The returned list is thread-safe for (PrioritizedListener element : list) { try { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketReceiving(event); } catch (Throwable e) { // Minecraft doesn't want your Exception. @@ -67,15 +68,14 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu return; for (PrioritizedListener element : list) { - final PacketListener listener = element.getListener(); - try { - if (listener.getReceivingWhitelist().getPriority() == priorityFilter) { - listener.onPacketReceiving(event); + if (element.getPriority() == priorityFilter) { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); + element.getListener().onPacketReceiving(event); } } catch (Throwable e) { // Minecraft doesn't want your Exception. - reporter.reportMinimal(listener.getPlugin(), "onPacketReceiving(PacketEvent)", e, + reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e, event.getPacket().getHandle()); } } @@ -94,6 +94,7 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu for (PrioritizedListener element : list) { try { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketSending(event); } catch (Throwable e) { // Minecraft doesn't want your Exception. @@ -116,15 +117,14 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu return; for (PrioritizedListener element : list) { - final PacketListener listener = element.getListener(); - try { - if (listener.getSendingWhitelist().getPriority() == priorityFilter) { - listener.onPacketSending(event); + if (element.getPriority() == priorityFilter) { + event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); + element.getListener().onPacketSending(event); } } catch (Throwable e) { // Minecraft doesn't want your Exception. - reporter.reportMinimal(listener.getPlugin(), "onPacketSending(PacketEvent)", e, + reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e, event.getPacket().getHandle()); } } From ed9b61fd11e2b2857f36ed44b7d435a1fe68bd5c Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 12 Mar 2013 02:33:35 +0100 Subject: [PATCH 05/46] Use an atomic reference array instead of ConcurrentHashMap for listeners --- .../protocol/async/PacketProcessingQueue.java | 3 +- .../AbstractConcurrentListenerMultimap.java | 40 +++++++++++-------- .../injector/SortedPacketListenerList.java | 5 +++ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java index 800124f2..3671ba0e 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java @@ -23,6 +23,7 @@ import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.Semaphore; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.PrioritizedListener; @@ -66,7 +67,7 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap { // The core of our map - private ConcurrentMap>> listeners = - new ConcurrentHashMap>>(); + private AtomicReferenceArray>> arrayListeners; + private ConcurrentMap>> mapListeners; + + public AbstractConcurrentListenerMultimap(int maximumPacketID) { + arrayListeners = new AtomicReferenceArray>>(maximumPacketID + 1); + mapListeners = new ConcurrentHashMap>>(); + } /** * Adds a listener to its requested list of packet recievers. @@ -53,20 +59,20 @@ public abstract class AbstractConcurrentListenerMultimap { // Add the listener to a specific packet notifcation list private void addListener(Integer packetID, PrioritizedListener listener) { - SortedCopyOnWriteArray> list = listeners.get(packetID); + SortedCopyOnWriteArray> list = arrayListeners.get(packetID); // We don't want to create this for every lookup if (list == null) { // It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order, // which is a essential feature for our purposes. final SortedCopyOnWriteArray> value = new SortedCopyOnWriteArray>(); - - list = listeners.putIfAbsent(packetID, value); - - // We may end up creating multiple multisets, but we'll agree - // on the one to use. - if (list == null) { + + // We may end up creating multiple multisets, but we'll agree on which to use + if (arrayListeners.compareAndSet(packetID, null, value)) { + mapListeners.put(packetID, value); list = value; + } else { + list = arrayListeners.get(packetID); } } @@ -85,8 +91,7 @@ public abstract class AbstractConcurrentListenerMultimap { // Again, not terribly efficient. But adding or removing listeners should be a rare event. for (Integer packetID : whitelist.getWhitelist()) { - - SortedCopyOnWriteArray> list = listeners.get(packetID); + SortedCopyOnWriteArray> list = arrayListeners.get(packetID); // Remove any listeners if (list != null) { @@ -96,7 +101,8 @@ public abstract class AbstractConcurrentListenerMultimap { list.remove(new PrioritizedListener(listener, whitelist.getPriority())); if (list.size() == 0) { - listeners.remove(packetID); + arrayListeners.set(packetID, null); + mapListeners.remove(packetID); removedPackets.add(packetID); } } @@ -116,7 +122,7 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Registered listeners. */ public Collection> getListener(int packetID) { - return listeners.get(packetID); + return arrayListeners.get(packetID); } /** @@ -124,7 +130,7 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Every listener. */ public Iterable> values() { - return Iterables.concat(listeners.values()); + return Iterables.concat(mapListeners.values()); } /** @@ -132,13 +138,15 @@ public abstract class AbstractConcurrentListenerMultimap { * @return Registered packet ID. */ public Set keySet() { - return listeners.keySet(); + return mapListeners.keySet(); } /** * Remove all packet listeners. */ protected void clearListeners() { - listeners.clear(); + arrayListeners = new AtomicReferenceArray< + SortedCopyOnWriteArray>>(arrayListeners.length()); + mapListeners.clear(); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java index 53fe6366..e1e3d912 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -19,6 +19,7 @@ package com.comphenix.protocol.injector; import java.util.Collection; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListenerPriority; @@ -31,6 +32,10 @@ import com.comphenix.protocol.events.PacketListener; * @author Kristian */ public final class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { + public SortedPacketListenerList() { + super(Packets.MAXIMUM_PACKET_ID); + } + /** * Invokes the given packet event for every registered listener. * @param reporter - the error reporter that will be used to inform about listener exceptions. From da0bcaa482b14069c860d2271156ddd67efc4037 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Wed, 13 Mar 2013 23:59:13 +0100 Subject: [PATCH 06/46] Seems to pass all the preliminary tests. --- .../src/main/java/com/comphenix/protocol/ProtocolLibrary.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 68fc0808..fed4ccc0 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -59,7 +59,7 @@ public class ProtocolLibrary extends JavaPlugin { /** * The maximum version ProtocolLib has been tested with, */ - private static final String MAXIMUM_MINECRAFT_VERSION = "1.4.7"; + private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.0"; /** * The number of milliseconds per second. From 310fd18e8953c628c48d32891e7a33e1aac85c52 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 17 Mar 2013 23:55:08 +0100 Subject: [PATCH 07/46] Ignore players that have logged out and have not been injected. --- ItemDisguise/.classpath | 6 ++++++ .../injector/player/ProxyPlayerInjectionHandler.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/ItemDisguise/.classpath b/ItemDisguise/.classpath index 71e70473..2bda6dc7 100644 --- a/ItemDisguise/.classpath +++ b/ItemDisguise/.classpath @@ -7,6 +7,12 @@ + + + + + + diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java index 5a9be055..6816610b 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 @@ -537,6 +537,11 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { 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); From d643690eef77b20263f615eaadab7c1a1df712e4 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Thu, 21 Mar 2013 02:10:30 +0100 Subject: [PATCH 08/46] Added a small patch for Libigot. --- .../protocol/utility/MinecraftReflection.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 399082d1..d5bf2329 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -129,6 +129,9 @@ public class MinecraftReflection { Class craftClass = craftServer.getClass(); CRAFTBUKKIT_PACKAGE = getPackage(craftClass.getCanonicalName()); + // Libigot patch + handleLibigot(); + // Next, do the same for CraftEntity.getHandle() in order to get the correct Minecraft package Class craftEntity = getCraftEntityClass(); Method getHandle = craftEntity.getMethod("getHandle"); @@ -165,6 +168,20 @@ public class MinecraftReflection { throw new IllegalStateException("Could not find Bukkit. Is it running?"); } } + + // Patch for Libigot + private static void handleLibigot() { + try { + getCraftEntityClass(); + } catch (RuntimeException e) { + // Try reverting the package to the old format + craftbukkitPackage = null; + CRAFTBUKKIT_PACKAGE = "org.bukkit.craftbukkit"; + + // This might fail too + getCraftEntityClass(); + } + } /** * Used during debugging and testing. From a43428c2c40d00ea5f29ac206e51d45e044728de Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sat, 23 Mar 2013 15:45:57 +0100 Subject: [PATCH 09/46] No need to create the dependency reduced POM --- ProtocolLib/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 586f9634..57a59bcd 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -57,7 +57,7 @@ false - true + false From b3322b35c1b4a6833ad5ac0a3c45d648fc0301f7 Mon Sep 17 00:00:00 2001 From: Kristian Stangeland Date: Thu, 28 Mar 2013 21:58:06 -0400 Subject: [PATCH 10/46] Prevent ConcurrentModifcationExceptions. See http://pastebin.com/UBvGSFs9/ --- .../protocol/reflect/compiler/BackgroundCompiler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java index 836a45d1..26c937ea 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -198,6 +198,11 @@ public class BackgroundCompiler { synchronized (listenerLock) { list = listeners.get(key); + + // Prevent ConcurrentModificationExceptions + if (list != null) { + list = Lists.newArrayList(list); + } } // Only execute the listeners if there is a list From 27da638a917565dca61ecb31d033f3176ab283d3 Mon Sep 17 00:00:00 2001 From: Kristian Stangeland Date: Thu, 28 Mar 2013 22:27:58 -0400 Subject: [PATCH 11/46] Update the player instance on login (LOWEST), not HIGHEST. Should ensure that packet listeners recieve the most up-to-date player instance, regardless of whether or not the main thread is blocked in the player listener. No more temporary players. --- .../injector/PacketFilterManager.java | 1 + .../player/PlayerInjectionHandler.java | 6 + .../injector/player/PlayerInjector.java | 5 +- .../player/ProxyPlayerInjectionHandler.java | 13 +++ .../injector/server/BukkitSocketInjector.java | 103 ++++++++++++++++++ .../injector/server/SocketInjector.java | 6 + .../injector/spigot/DummyPlayerHandler.java | 5 + 7 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 28b79198..44aa1e46 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -696,6 +696,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok try { // Let's clean up the other injection first. playerInjection.uninjectPlayer(event.getPlayer().getAddress()); + playerInjection.updatePlayer(event.getPlayer()); } catch (Exception e) { reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject net handler for player.", e, event); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java index b26769fa..85669c69 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java @@ -129,6 +129,12 @@ public interface PlayerInjectionHandler { public abstract void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException; + /** + * Ensure that packet readers are informed of this player reference. + * @param player - the player to update. + */ + public abstract void updatePlayer(Player player); + /** * Determine if the given listeners are valid. * @param version - the current Minecraft version, or NULL if unknown. 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 a2fa3362..caff1e2c 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 @@ -642,10 +642,7 @@ abstract class PlayerInjector implements SocketInjector { // Do nothing } - /** - * Set the real Bukkit player that we will use. - * @param updatedPlayer - the real Bukkit player. - */ + @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 6816610b..2329d6b1 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 @@ -42,6 +42,7 @@ 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; @@ -416,6 +417,18 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { } } + @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. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java new file mode 100644 index 00000000..8e72db01 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/BukkitSocketInjector.java @@ -0,0 +1,103 @@ +package com.comphenix.protocol.injector.server; + +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.bukkit.entity.Player; + +public class BukkitSocketInjector implements SocketInjector { + /** + * Represents a single send packet command. + * @author Kristian + */ + static class SendPacketCommand { + private final Object packet; + private final boolean filtered; + + public SendPacketCommand(Object packet, boolean filtered) { + this.packet = packet; + this.filtered = filtered; + } + + public Object getPacket() { + return packet; + } + + public boolean isFiltered() { + return filtered; + } + } + + private Player player; + + // Queue of server packets + private List syncronizedQueue = Collections.synchronizedList(new ArrayList()); + + /** + * Represents a temporary socket injector. + * @param temporaryPlayer - + */ + public BukkitSocketInjector(Player player) { + if (player == null) + throw new IllegalArgumentException("Player cannot be NULL."); + this.player = player; + } + + @Override + public Socket getSocket() throws IllegalAccessException { + throw new UnsupportedOperationException("Cannot get socket from Bukkit player."); + } + + @Override + public SocketAddress getAddress() throws IllegalAccessException { + return player.getAddress(); + } + + @Override + public void disconnect(String message) throws InvocationTargetException { + player.kickPlayer(message); + } + + @Override + public void sendServerPacket(Object packet, boolean filtered) + throws InvocationTargetException { + SendPacketCommand command = new SendPacketCommand(packet, filtered); + + // Queue until we can find something better + syncronizedQueue.add(command); + } + + @Override + public Player getPlayer() { + return player; + } + + @Override + public Player getUpdatedPlayer() { + return player; + } + + @Override + public void transferState(SocketInjector delegate) { + // Transmit all queued packets to a different injector. + try { + synchronized(syncronizedQueue) { + for (SendPacketCommand command : syncronizedQueue) { + delegate.sendServerPacket(command.getPacket(), command.isFiltered()); + } + syncronizedQueue.clear(); + } + } catch (InvocationTargetException e) { + throw new RuntimeException("Unable to transmit packets to " + delegate + " from old injector.", e); + } + } + + @Override + public void setUpdatedPlayer(Player updatedPlayer) { + this.player = updatedPlayer; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java index 6407d320..e484c2e5 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java @@ -58,4 +58,10 @@ public interface SocketInjector { * @param delegate - the new injector. */ public abstract void transferState(SocketInjector delegate); + + /** + * Set the real Bukkit player that we will use. + * @param updatedPlayer - the real Bukkit player. + */ + public abstract void setUpdatedPlayer(Player updatedPlayer); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java index 08b1a8d2..8df7f70c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java @@ -119,4 +119,9 @@ class DummyPlayerHandler implements PlayerInjectionHandler { public void postWorldLoaded() { // Do nothing } + + @Override + public void updatePlayer(Player player) { + // Do nothing + } } From 9a34036d14107481814d3521d5a94a8e911066ac Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 2 Apr 2013 13:55:18 +0200 Subject: [PATCH 12/46] Improve Minecraft class detection --- .../protocol/utility/MinecraftReflection.java | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index d5bf2329..bb3fed3c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -81,6 +81,9 @@ public class MinecraftReflection { private static Constructor craftNMSConstructor; private static Constructor craftBukkitConstructor; + // Matches classes + private static AbstractFuzzyMatcher> fuzzyMatcher; + // New in 1.4.5 private static Method craftNMSMethod; private static Method craftBukkitMethod; @@ -88,7 +91,12 @@ public class MinecraftReflection { // net.minecraft.server private static Class itemStackArrayClass; - + + /** + * Whether or not we're currently initializing the reflection handler. + */ + private static boolean initializing; + private MinecraftReflection() { // No need to make this constructable. } @@ -108,7 +116,9 @@ public class MinecraftReflection { * @return A matcher for Minecraft objects. */ public static AbstractFuzzyMatcher> getMinecraftObjectMatcher() { - return FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50); + if (fuzzyMatcher == null) + fuzzyMatcher = FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50); + return fuzzyMatcher; } /** @@ -119,6 +129,9 @@ public class MinecraftReflection { // Speed things up if (MINECRAFT_FULL_PACKAGE != null) return MINECRAFT_FULL_PACKAGE; + if (initializing) + throw new IllegalStateException("Already initializing minecraft package!"); + initializing = true; Server craftServer = Bukkit.getServer(); @@ -144,16 +157,16 @@ public class MinecraftReflection { MINECRAFT_PREFIX_PACKAGE = MINECRAFT_FULL_PACKAGE; // The package is usualy flat, so go with that assumtion - DYNAMIC_PACKAGE_MATCHER = + String matcher = (MINECRAFT_PREFIX_PACKAGE.length() > 0 ? Pattern.quote(MINECRAFT_PREFIX_PACKAGE + ".") : "") + "\\w+"; // We'll still accept the default location, however - DYNAMIC_PACKAGE_MATCHER = "(" + DYNAMIC_PACKAGE_MATCHER + ")|(" + MINECRAFT_OBJECT + ")"; + setDynamicPackageMatcher("(" + matcher + ")|(" + MINECRAFT_OBJECT + ")"); } else { // Use the standard matcher - DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT; + setDynamicPackageMatcher(MINECRAFT_OBJECT); } return MINECRAFT_FULL_PACKAGE; @@ -162,13 +175,27 @@ public class MinecraftReflection { throw new RuntimeException("Security violation. Cannot get handle method.", e); } catch (NoSuchMethodException e) { throw new IllegalStateException("Cannot find getHandle() method on server. Is this a modified CraftBukkit version?", e); + } finally { + initializing = false; } } else { + initializing = false; throw new IllegalStateException("Could not find Bukkit. Is it running?"); } } + /** + * Update the dynamic package matcher. + * @param regex - the Minecraft package regex. + */ + private static void setDynamicPackageMatcher(String regex) { + DYNAMIC_PACKAGE_MATCHER = regex; + + // Ensure that the matcher is regenerated + fuzzyMatcher = null; + } + // Patch for Libigot private static void handleLibigot() { try { @@ -193,7 +220,7 @@ public class MinecraftReflection { CRAFTBUKKIT_PACKAGE = craftBukkitPackage; // Standard matcher - DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT; + setDynamicPackageMatcher(MINECRAFT_OBJECT); } /** @@ -261,8 +288,7 @@ public class MinecraftReflection { if (clazz == null) throw new IllegalArgumentException("Class cannot be NULL."); - // Doesn't matter if we don't check for the version here - return clazz.getName().startsWith(MINECRAFT_PREFIX_PACKAGE); + return getMinecraftObjectMatcher().isMatch(clazz, null); } /** From fb441b4910ab2fde84cf4f2938afb9e2ad976d86 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 2 Apr 2013 14:12:36 +0200 Subject: [PATCH 13/46] Retrieve the correct Minecraft superclass --- .../protocol/injector/player/NetworkServerInjector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 207bcba3..610d1a8b 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 @@ -249,7 +249,7 @@ class NetworkServerInjector extends PlayerInjector { } private Class getFirstMinecraftSuperClass(Class clazz) { - if (clazz.getName().startsWith(MinecraftReflection.getMinecraftPackage())) + if (MinecraftReflection.isMinecraftClass(clazz)) return clazz; else if (clazz.equals(Object.class)) return clazz; From 22beae23e0c5184ef8a8fd8e76ea4c7280461222 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 2 Apr 2013 14:28:34 +0200 Subject: [PATCH 14/46] Retrieve the correct Nbt base class --- .../comphenix/protocol/utility/MinecraftReflection.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index bb3fed3c..9261f7bb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -852,9 +852,15 @@ public class MinecraftReflection { returnTypeMatches(tagCompoundContract). build() ); + Class nbtBase = selected.getReturnType().getSuperclass(); + // That can't be correct + if (nbtBase == null || nbtBase.equals(Object.class)) { + throw new IllegalStateException("Unable to find NBT base class: " + nbtBase); + } + // Use the return type here too - return setMinecraftClass("NBTBase", selected.getReturnType()); + return setMinecraftClass("NBTBase", nbtBase); } } From 15980d70fbfe1e213ec3e566ac5925035e224eed Mon Sep 17 00:00:00 2001 From: Kristian Date: Sun, 7 Apr 2013 15:33:19 +0200 Subject: [PATCH 15/46] Added a simple filter system that utilizes JavaScript (Rhino) This makes it possible to filter packet events with arbitrary code. --- .../com/comphenix/protocol/CommandBase.java | 2 + .../com/comphenix/protocol/CommandFilter.java | 394 ++++++++++++++++++ .../com/comphenix/protocol/CommandPacket.java | 11 +- .../protocol/MultipleLinesPrompt.java | 81 ++++ .../comphenix/protocol/ProtocolLibrary.java | 5 +- .../protocol/concurrency/IntegerSet.java | 11 + ProtocolLib/src/main/resources/plugin.yml | 6 + 7 files changed, 505 insertions(+), 5 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java index 18ddb25e..1b8cd00c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java @@ -55,6 +55,7 @@ abstract class CommandBase implements CommandExecutor { try { // Make sure we're dealing with the correct command if (!command.getName().equalsIgnoreCase(name)) { + reporter.reportWarning(this, "Incorrect command assigned to " + this); return false; } if (permission != null && !sender.hasPermission(permission)) { @@ -66,6 +67,7 @@ abstract class CommandBase implements CommandExecutor { if (args != null && args.length >= minimumArgumentCount) { return handleCommand(sender, args); } else { + sender.sendMessage(ChatColor.RED + "Insufficient commands. You need at least " + minimumArgumentCount); return false; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java new file mode 100644 index 00000000..de13c50a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -0,0 +1,394 @@ +package com.comphenix.protocol; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.conversations.Conversable; +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationAbandonedEvent; +import org.bukkit.conversations.ConversationAbandonedListener; +import org.bukkit.conversations.ConversationCanceller; +import org.bukkit.conversations.ConversationContext; +import org.bukkit.conversations.ConversationFactory; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.PacketEvent; +import com.google.common.collect.DiscreteDomains; +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; + +/** + * A command to apply JavaScript filtering to the packet command. + * + * @author Kristian + */ +public class CommandFilter extends CommandBase { + @SuppressWarnings("serial") + public static class FilterFailedException extends RuntimeException { + private Filter filter; + + public FilterFailedException() { + super(); + } + + public FilterFailedException(String message, Filter filter, Throwable cause) { + super(message, cause); + this.filter = filter; + } + + public Filter getFilter() { + return filter; + } + } + /** + * Possible sub commands. + * + * @author Kristian + */ + private enum SubCommand { + ADD, REMOVE; + } + + /** + * A filter that will be used to process a packet event. + * @author Kristian + */ + public static class Filter { + private final String name; + private final String predicate; + + private final IntegerSet ranges; + + /** + * Construct a new immutable filter. + * @param name - the unique name of the filter. + * @param predicate - the JavaScript predicate that will be used to filter packet events. + * @param ranges - a list of valid packet ID ranges that this filter applies to. + */ + public Filter(String name, String predicate, Set packets) { + this.name = name; + this.predicate = predicate; + this.ranges = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + this.ranges.addAll(packets); + } + + /** + * Retrieve the unique name of the filter. + * @return Unique name of the filter. + */ + public String getName() { + return name; + } + + /** + * Retrieve the JavaScript predicate that will be used to filter packet events. + * @return Predicate itself. + */ + public String getPredicate() { + return predicate; + } + + /** + * Retrieve a copy of the set of packets this filter applies to. + * @return Set of packets this filter applies to. + */ + public Set getRanges() { + return ranges.toSet(); + } + + /** + * Determine whether or not a packet event needs to be passed to this filter. + * @param event - the event to test. + * @return TRUE if it does, FALSE otherwise. + */ + private boolean isApplicable(PacketEvent event) { + return ranges.contains(event.getPacketID()); + } + + /** + * Evaluate the current filter using the provided ScriptEngine as context. + *

+ * This context may be modified with additional code. + * @param context - the current script context. + * @param event - the packet event to evaluate. + * @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise. + * @throws ScriptException If the compilation failed. + */ + public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException { + if (!isApplicable(event)) + return true; + // Ensure that the predicate has been compiled + compile(context); + + try { + return (Boolean) ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle()); + } catch (NoSuchMethodException e) { + // Must be a fault with the script engine itself + throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e); + } + } + + /** + * Force the compilation of a specific filter. + * @param context - the current script context. + * @throws ScriptException If the compilation failed. + */ + public void compile(ScriptEngine context) throws ScriptException { + if (context.get(name) == null) { + context.eval("var " + name + " = function(event, packet) {\n" + predicate); + } + } + + /** + * Clean up all associated code from this filter in the provided script engine. + * @param context - the current script context. + */ + public void close(ScriptEngine context) { + context.put(name, null); + } + } + + private static class BracketBalance implements ConversationCanceller { + private String KEY_BRACKET_COUNT = "bracket_balance.count"; + + // What to set the initial counter + private final int initialBalance; + + public BracketBalance(int initialBalance) { + this.initialBalance = initialBalance; + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String in) { + Object stored = context.getSessionData(KEY_BRACKET_COUNT); + int value = 0; + + // Get the stored value + if (stored instanceof Integer) { + value = (Integer)stored; + } else { + value = initialBalance; + } + + value += count(in, '{') - count(in, '}'); + context.setSessionData(KEY_BRACKET_COUNT, value); + + // Cancel if the bracket balance is zero + return value <= 0; + } + + private int count(String text, char character) { + int counter = 0; + + for (int i=0; i < text.length(); i++) { + if (text.charAt(i) == character) { + counter++; + } + } + return counter; + } + + @Override + public void setConversation(Conversation conversation) { + // Whatever + } + + @Override + public ConversationCanceller clone() { + return new BracketBalance(initialBalance); + } + } + + /** + * Name of this command. + */ + public static final String NAME = "filter"; + + // Currently registered filters + private List filters = new ArrayList(); + + // Owner plugin + private final Plugin plugin; + + // Script engine + private ScriptEngine engine; + + public CommandFilter(ErrorReporter reporter, Plugin plugin) { + super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 2); + this.plugin = plugin; + + // Start the engine + initalizeScript(); + } + + private void initalizeScript() { + ScriptEngineManager manager = new ScriptEngineManager(); + engine = manager.getEngineByName("JavaScript"); + + // Import useful packages + try { + engine.eval("importPackage(org.bukkit);"); + engine.eval("importPackage(com.comphenix.protocol.reflect);"); + } catch (ScriptException e) { + throw new IllegalStateException("Unable to initialize packages for JavaScript engine.", e); + } + } + + /** + * Determine whether or not to pass the given packet event to the packet listeners. + * @param event - the event. + * @return TRUE if we should, FALSE otherwise. + * @throws FilterFailedException If one of the filters failed. + */ + public boolean filterEvent(PacketEvent event) throws FilterFailedException { + for (Filter filter : filters) { + try { + if (!filter.evaluate(engine, event)) { + return false; + } + } catch (ScriptException e) { + throw new FilterFailedException("Filter failed.", filter, e); + } + } + // Pass! + return true; + } + + /* + * Description: Adds or removes a simple packet listener. + Usage: / add|remove name [packet IDs] + */ + @Override + protected boolean handleCommand(CommandSender sender, String[] args) { + final SubCommand command = parseCommand(args, 0); + final String name = args[1]; + + switch (command) { + case ADD: + // Never overwrite an existing filter + if (findFilter(name) != null) { + sender.sendMessage(ChatColor.RED + "Filter " + name + " already exists. Remove it first."); + return true; + } + + final Set packets = parseRanges(args, 2); + sender.sendMessage("Enter filter program ('}' to complete or CANCEL):"); + + // Make sure we can use the conversable interface + if (sender instanceof Conversable) { + final MultipleLinesPrompt prompt = + new MultipleLinesPrompt(new BracketBalance(1), "function(event, packet) {"); + + new ConversationFactory(plugin). + withFirstPrompt(prompt). + withEscapeSequence("CANCEL"). + withLocalEcho(false). + addConversationAbandonedListener(new ConversationAbandonedListener() { + @Override + public void conversationAbandoned(ConversationAbandonedEvent event) { + try { + final Conversable whom = event.getContext().getForWhom(); + + if (event.gracefulExit()) { + String predicate = prompt.removeAccumulatedInput(event.getContext()); + Filter filter = new Filter(name, predicate, packets); + + // Print the last line as well + whom.sendRawMessage(prompt.getPromptText(event.getContext())); + + try { + // Force early compilation + filter.compile(engine); + + filters.add(filter); + whom.sendRawMessage(ChatColor.GOLD + "Added filter " + name); + } catch (ScriptException e) { + e.printStackTrace(); + whom.sendRawMessage(ChatColor.GOLD + "Compilation error: " + e.getMessage()); + } + } else { + // Too bad + whom.sendRawMessage(ChatColor.RED + "Cancelled filter."); + } + } catch (Exception e) { + reporter.reportDetailed(this, "Cannot handle conversation.", e, event); + } + } + }). + buildConversation((Conversable) sender). + begin(); + } else { + sender.sendMessage(ChatColor.RED + "Only console and players are supported!"); + } + + break; + + case REMOVE: + Filter filter = findFilter(name); + + // See if it exists before we remove it + if (filter != null) { + filter.close(engine); + filters.remove(filter); + sender.sendMessage(ChatColor.GOLD + "Removed filter " + name); + } else { + sender.sendMessage(ChatColor.RED + "Unable to find a filter by the name " + name); + } + break; + } + + return true; + } + + private Set parseRanges(String[] args, int start) { + List> ranges = RangeParser.getRanges(args, 2, args.length - 1, Ranges.closed(0, 255)); + Set flatten = new HashSet(); + + if (ranges.isEmpty()) { + // Use every packet ID + ranges.add(Ranges.closed(0, 255)); + } + + // Finally, flatten it all + for (Range range : ranges) { + flatten.addAll(range.asSet(DiscreteDomains.integers())); + } + return flatten; + } + + /** + * Lookup a filter by its name. + * @param name - the filter name. + * @return The filter, or NULL if not found. + */ + private Filter findFilter(String name) { + // We'll just use a linear scan for now - we don't expect that many filters + for (Filter filter : filters) { + if (filter.getName().equalsIgnoreCase(name)) { + return filter; + } + } + return null; + } + + private SubCommand parseCommand(String[] args, int index) { + String text = args[index].toUpperCase(); + + try { + return SubCommand.valueOf(text); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.", e); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index 6f098599..3d67453a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -93,11 +93,15 @@ class CommandPacket extends CommandBase { private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); - public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, ProtocolManager manager) { + // Filter packet events + private CommandFilter filter; + + public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) { super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); this.plugin = plugin; this.logger = logger; this.manager = manager; + this.filter = filter; this.chatter = new ChatExtensions(manager); } @@ -362,7 +366,6 @@ class CommandPacket extends CommandBase { } public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) { - Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers()); Set packets; @@ -386,14 +389,14 @@ class CommandPacket extends CommandBase { return new DetailedPacketListener() { @Override public void onPacketSending(PacketEvent event) { - if (side.isForServer()) { + if (side.isForServer() && filter.filterEvent(event)) { printInformation(event); } } @Override public void onPacketReceiving(PacketEvent event) { - if (side.isForClient()) { + if (side.isForClient() && filter.filterEvent(event)) { printInformation(event); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java new file mode 100644 index 00000000..48695a60 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java @@ -0,0 +1,81 @@ +package com.comphenix.protocol; + +import org.bukkit.conversations.ConversationCanceller; +import org.bukkit.conversations.ConversationContext; +import org.bukkit.conversations.ExactMatchConversationCanceller; +import org.bukkit.conversations.Prompt; +import org.bukkit.conversations.StringPrompt; + +/** + * Represents a conversation prompt that accepts a list of lines. + * + * @author Kristian + */ +class MultipleLinesPrompt extends StringPrompt { + // Feels a bit like Android + private static final String KEY = "multiple_lines_prompt"; + private static final String KEY_LAST = KEY + ".last_line"; + + private final ConversationCanceller endMarker; + private final String initialPrompt; + + /** + * Retrieve and remove the current accumulated input. + * @param context - conversation context. + * @return The accumulated input, or NULL if not found. + */ + public String removeAccumulatedInput(ConversationContext context) { + Object result = context.getSessionData(KEY); + + if (result instanceof StringBuilder) { + context.setSessionData(KEY, null); + return ((StringBuilder) result).toString(); + } else { + return null; + } + } + + /** + * Construct a multiple lines input prompt with a specific end marker. + *

+ * This is usually an empty string. + * @param endMarker - the end marker. + */ + public MultipleLinesPrompt(String endMarker, String initialPrompt) { + this(new ExactMatchConversationCanceller(endMarker), initialPrompt); + } + + public MultipleLinesPrompt(ConversationCanceller endMarker, String initialPrompt) { + this.endMarker = endMarker; + this.initialPrompt = initialPrompt; + } + + @Override + public Prompt acceptInput(ConversationContext context, String in) { + StringBuilder result = (StringBuilder) context.getSessionData(KEY); + + if (result == null) { + context.setSessionData(KEY, result = new StringBuilder()); + } + + // Save the last line as well + context.setSessionData(KEY_LAST, in); + result.append(in); + + // And we're done + if (endMarker.cancelBasedOnInput(context, in)) + return Prompt.END_OF_CONVERSATION; + else + return this; + } + + @Override + public String getPromptText(ConversationContext context) { + Object last = context.getSessionData(KEY_LAST); + + if (last instanceof String) + return (String) last; + else + return initialPrompt; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index fed4ccc0..dcb120c5 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -103,6 +103,7 @@ public class ProtocolLibrary extends JavaPlugin { // Commands private CommandProtocol commandProtocol; private CommandPacket commandPacket; + private CommandFilter commandFilter; // Whether or not disable is not needed private boolean skipDisable; @@ -161,7 +162,8 @@ public class ProtocolLibrary extends JavaPlugin { // Initialize command handlers commandProtocol = new CommandProtocol(detailedReporter, this, updater, config); - commandPacket = new CommandPacket(detailedReporter, this, logger, protocolManager); + commandFilter = new CommandFilter(detailedReporter, this); + commandPacket = new CommandPacket(detailedReporter, this, logger, commandFilter, protocolManager); // Send logging information to player listeners too setupBroadcastUsers(PERMISSION_INFO); @@ -256,6 +258,7 @@ public class ProtocolLibrary extends JavaPlugin { // Set up command handlers registerCommand(CommandProtocol.NAME, commandProtocol); registerCommand(CommandPacket.NAME, commandPacket); + registerCommand(CommandFilter.NAME, commandFilter); // Player login and logout events protocolManager.registerEvents(manager, this); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java index e2aaa3f6..c99e18e1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java @@ -18,6 +18,7 @@ package com.comphenix.protocol.concurrency; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -60,6 +61,16 @@ public class IntegerSet { array[element] = true; } + /** + * Add the given collection of elements to the set. + * @param packets - elements to add. + */ + public void addAll(Collection packets) { + for (Integer id : packets) { + add(id); + } + } + /** * Remove the given element from the set, or do nothing if it's already removed. * @param element - element to remove. diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index c95bbdcf..1b2a0377 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -17,6 +17,12 @@ commands: usage: / add|remove|names client|server [ID start]-[ID stop] [detailed] permission: protocol.admin permission-message: You don't have + filter: + description: Add or remove programmable filters to the packet listeners. + usage: / add|remove name [ID start]-[ID stop] + aliases: [packet_filter] + permission: protocol.admin + permission-message: You don't have permissions: protocol.*: From 3ee38d7b6d3d65c5a4f033589f05f2965cbde478 Mon Sep 17 00:00:00 2001 From: Kristian Date: Sun, 7 Apr 2013 15:57:01 +0200 Subject: [PATCH 16/46] Arbitrary code execution is very dangerous. Limit to debug mode. The filter command allows users with sufficient permission (or OPs) to execute arbitrary JavaScript (no sandboxing). This is fine for a debug and testing, but could potentially be exploited in a production environment. Instead, we disable this command by default and force users to enable it specifically in the configuration file (not through commands). If someone has access to the config.yml file, they probably also have access to the plugins/ folder and thus the ability to install plugins with arbitrary code execution as well. --- .../com/comphenix/protocol/CommandFilter.java | 13 ++++++++++-- .../comphenix/protocol/ProtocolConfig.java | 20 +++++++++++++++++++ .../comphenix/protocol/ProtocolLibrary.java | 7 ++++++- ProtocolLib/src/main/resources/config.yml | 5 ++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index de13c50a..6f60277e 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -221,12 +221,16 @@ public class CommandFilter extends CommandBase { // Owner plugin private final Plugin plugin; + // Whether or not the command is enabled + private ProtocolConfig config; + // Script engine private ScriptEngine engine; - public CommandFilter(ErrorReporter reporter, Plugin plugin) { + 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(); @@ -264,13 +268,18 @@ public class CommandFilter extends CommandBase { // Pass! return true; } - + /* * Description: Adds or removes a simple packet listener. Usage: / add|remove name [packet IDs] */ @Override protected boolean handleCommand(CommandSender sender, String[] args) { + if (!config.isDebug()) { + sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!"); + return true; + } + final SubCommand command = parseCommand(args, 0); final String name = args[1]; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index 87e5b523..4ce0bf06 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -40,6 +40,8 @@ class ProtocolConfig { private static final String IGNORE_VERSION_CHECK = "ignore version check"; private static final String BACKGROUND_COMPILER_ENABLED = "background compiler"; + private static final String DEBUG_MODE_ENABLED = "debug"; + private static final String INJECTION_METHOD = "injection method"; private static final String UPDATER_NOTIFY = "notify"; @@ -140,6 +142,24 @@ class ProtocolConfig { public void setAutoDownload(boolean value) { updater.set(UPDATER_DOWNLAD, value); } + + /** + * Determine whether or not debug mode is enabled. + *

+ * This grants access to the filter command. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean isDebug() { + return global.getBoolean(DEBUG_MODE_ENABLED, false); + } + + /** + * Set whether or not debug mode is enabled. + * @param value - TRUE if it is enabled, FALSE otherwise. + */ + public void setDebug(boolean value) { + global.set(DEBUG_MODE_ENABLED, value); + } /** * Retrieve the amount of time to wait until checking for a new update. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index dcb120c5..7304eabb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -130,6 +130,11 @@ public class ProtocolLibrary extends JavaPlugin { } } + // Print the state of the debug mode + if (config.isDebug()) { + logger.warning("Debug mode is enabled!"); + } + try { // Check for other versions checkConflictingVersions(); @@ -162,7 +167,7 @@ public class ProtocolLibrary extends JavaPlugin { // Initialize command handlers commandProtocol = new CommandProtocol(detailedReporter, this, updater, config); - commandFilter = new CommandFilter(detailedReporter, this); + commandFilter = new CommandFilter(detailedReporter, this, config); commandPacket = new CommandPacket(detailedReporter, this, logger, commandFilter, protocolManager); // Send logging information to player listeners too diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index 46869ad7..e9185886 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -18,4 +18,7 @@ global: ignore version check: # Override the starting injecting method - injection method: \ No newline at end of file + injection method: + + # Whether or not to enable the filter command + debug: false \ No newline at end of file From 867afe29f7fd15d672b12ecdee2d970e0e7c62d3 Mon Sep 17 00:00:00 2001 From: Kristian Date: Sun, 7 Apr 2013 15:57:38 +0200 Subject: [PATCH 17/46] Minecraft 1.5.1 seems to work fine. --- .../src/main/java/com/comphenix/protocol/ProtocolLibrary.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 7304eabb..1ddf25bf 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -59,7 +59,7 @@ public class ProtocolLibrary extends JavaPlugin { /** * The maximum version ProtocolLib has been tested with, */ - private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.0"; + private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.1"; /** * The number of milliseconds per second. From 505226f8adb08820b938705b160ddf0e54ae0286 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 8 Apr 2013 17:57:56 +0200 Subject: [PATCH 18/46] Added the ability to match derived classes and interfaces. --- .../reflect/fuzzy/AbstractFuzzyMatcher.java | 19 +++ .../reflect/fuzzy/FuzzyClassContract.java | 129 +++++++++++++++--- 2 files changed, 127 insertions(+), 21 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java index a93ea18f..0e522142 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java @@ -62,6 +62,25 @@ public abstract class AbstractFuzzyMatcher implements Comparable obj) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java index bb1bfcdb..3a15ab44 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java @@ -1,6 +1,7 @@ package com.comphenix.protocol.reflect.fuzzy; import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -23,6 +24,9 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { private final ImmutableList> methodContracts; private final ImmutableList> constructorContracts; + private final ImmutableList>> baseclassContracts; + private final ImmutableList>> interfaceContracts; + /** * Represents a class contract builder. * @author Kristian @@ -33,6 +37,9 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { private List> methodContracts = Lists.newArrayList(); private List> constructorContracts = Lists.newArrayList(); + private List>> baseclassContracts = Lists.newArrayList(); + private List>> interfaceContracts = Lists.newArrayList(); + /** * Add a new field contract. * @param matcher - new field contract. @@ -89,18 +96,54 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { public Builder constructor(FuzzyMethodContract.Builder builder) { return constructor(builder.build()); } + + /** + * Add a new base class contract. + * @param matcher - new base class contract. + * @return This builder, for chaining. + */ + public Builder baseclass(AbstractFuzzyMatcher> matcher) { + baseclassContracts.add(matcher); + return this; + } + + /** + * Add a new base class contract. + * @param matcher - builder for the new base class contract. + * @return This builder, for chaining. + */ + public Builder baseclass(FuzzyClassContract.Builder builder) { + return baseclass(builder.build()); + } + + /** + * Add a new interface contract. + * @param matcher - new interface contract. + * @return This builder, for chaining. + */ + public Builder interfaces(AbstractFuzzyMatcher> matcher) { + interfaceContracts.add(matcher); + return this; + } + + /** + * Add a new interface contract. + * @param matcher - builder for the new interface contract. + * @return This builder, for chaining. + */ + public Builder interfaces(FuzzyClassContract.Builder builder) { + return interfaces(builder.build()); + } public FuzzyClassContract build() { Collections.sort(fieldContracts); Collections.sort(methodContracts); Collections.sort(constructorContracts); + Collections.sort(baseclassContracts); + Collections.sort(interfaceContracts); // Construct a new class matcher - return new FuzzyClassContract( - ImmutableList.copyOf(fieldContracts), - ImmutableList.copyOf(methodContracts), - ImmutableList.copyOf(constructorContracts) - ); + return new FuzzyClassContract(this); } } @@ -114,17 +157,15 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { /** * Constructs a new fuzzy class contract with the given contracts. - * @param fieldContracts - field contracts. - * @param methodContracts - method contracts. - * @param constructorContracts - constructor contracts. + * @param builder - the builder that is constructing us. */ - private FuzzyClassContract(ImmutableList> fieldContracts, - ImmutableList> methodContracts, - ImmutableList> constructorContracts) { + private FuzzyClassContract(Builder builder) { super(); - this.fieldContracts = fieldContracts; - this.methodContracts = methodContracts; - this.constructorContracts = constructorContracts; + this.fieldContracts = ImmutableList.copyOf(builder.fieldContracts); + this.methodContracts = ImmutableList.copyOf(builder.methodContracts); + this.constructorContracts = ImmutableList.copyOf(builder.constructorContracts); + this.baseclassContracts = ImmutableList.copyOf(builder.baseclassContracts); + this.interfaceContracts = ImmutableList.copyOf(builder.interfaceContracts); } /** @@ -157,12 +198,34 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { return constructorContracts; } + /** + * Retrieve an immutable list of every baseclass contract. + *

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

+ * This list is ordered in descending order of priority. + * @return List of every interface contract. + */ + public ImmutableList>> getInterfaceContracts() { + return interfaceContracts; + } + @Override protected int calculateRoundNumber() { // Find the highest round number return combineRounds(findHighestRound(fieldContracts), - combineRounds(findHighestRound(methodContracts), - findHighestRound(constructorContracts))); + findHighestRound(methodContracts), + findHighestRound(constructorContracts), + findHighestRound(interfaceContracts), + findHighestRound(baseclassContracts)); } private int findHighestRound(Collection> list) { @@ -179,12 +242,19 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { FuzzyReflection reflection = FuzzyReflection.fromClass(value, true); // Make sure all the contracts are valid - return processContracts(reflection.getFields(), value, fieldContracts) && - processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts) && - processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts); + return (fieldContracts.size() == 0 || + processContracts(reflection.getFields(), value, fieldContracts)) && + (methodContracts.size() == 0 || + processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts)) && + (constructorContracts.size() == 0 || + processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts)) && + (baseclassContracts.size() == 0 || + processValue(value.getSuperclass(), parent, baseclassContracts)) && + (interfaceContracts.size() == 0 || + processContracts(Arrays.asList(value.getInterfaces()), (Class) parent, interfaceContracts)); } - private boolean processContracts(Collection values, Class parent, List> matchers) { + private boolean processContracts(Collection values, Object parent, List> matchers) { boolean[] accepted = new boolean[matchers.size()]; int count = accepted.length; @@ -205,7 +275,18 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { return count == 0; } - private int processValue(T value, Class parent, boolean accepted[], List> matchers) { + private boolean processValue(T value, Object parent, List> matchers) { + for (int i = 0; i < matchers.size(); i++) { + if (matchers.get(i).isMatch(value, parent)) { + return true; + } + } + + // No match + return false; + } + + private int processValue(T value, Object parent, boolean accepted[], List> matchers) { // The order matters for (int i = 0; i < matchers.size(); i++) { if (!accepted[i]) { @@ -235,6 +316,12 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher> { if (constructorContracts.size() > 0) { params.put("constructors", constructorContracts); } + if (baseclassContracts.size() > 0) { + params.put("baseclasses", baseclassContracts); + } + if (interfaceContracts.size() > 0) { + params.put("interfaces", interfaceContracts); + } return "{\n " + Joiner.on(", \n ").join(params.entrySet()) + "\n}"; } } From 82bb7a7c439c857d06aef0556b80bf80d906e84a Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 8 Apr 2013 21:53:54 +0200 Subject: [PATCH 19/46] Adding support for Spigot MCPC 1.2.5. Very buggy indeed. --- .../com/comphenix/protocol/CommandFilter.java | 2 +- .../com/comphenix/protocol/CommandPacket.java | 2 + .../comphenix/protocol/ProtocolLibrary.java | 2 +- .../injector/PacketFilterManager.java | 11 +- .../packet/PacketInjectorBuilder.java | 5 +- .../injector/packet/PacketRegistry.java | 65 ++++++++ .../injector/packet/ProxyPacketInjector.java | 157 ++++++++++++------ .../protocol/wrappers/TroveWrapper.java | 104 ++++++++++++ 8 files changed, 293 insertions(+), 55 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index 6f60277e..0ff5a162 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -270,7 +270,7 @@ public class CommandFilter extends CommandBase { } /* - * Description: Adds or removes a simple packet listener. + * Description: Adds or removes a simple packet filter. Usage: / add|remove name [packet IDs] */ @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index 3d67453a..d130677d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -340,6 +340,8 @@ class CommandPacket extends CommandBase { supported.addAll(Packets.Client.getSupported()); else if (side.isForServer()) supported.addAll(Packets.Server.getSupported()); + + System.out.println("Supported for " + side + ": " + supported); return supported; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 1ddf25bf..d1e27b6f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -259,7 +259,7 @@ public class ProtocolLibrary extends JavaPlugin { } else { logger.info("Structure compiler thread has been disabled."); } - + // Set up command handlers registerCommand(CommandProtocol.NAME, commandProtocol); registerCommand(CommandPacket.NAME, commandPacket); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 44aa1e46..af7453cc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -229,7 +229,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok reporter.reportWarning(this, "Cannot load server and client packet list.", e); } - } catch (IllegalAccessException e) { + } catch (FieldAccessException e) { reporter.reportWarning(this, "Unable to initialize packet injector.", e); } } @@ -757,7 +757,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (!MinecraftReflection.isPacketClass(packet)) throw new IllegalArgumentException("The given object " + packet + " is not a packet."); - return PacketRegistry.getPacketToID().get(packet.getClass()); + Integer id = PacketRegistry.getPacketToID().get(packet.getClass()); + + if (id != null) { + return id; + } else { + throw new IllegalArgumentException( + "Unable to find associated packet of " + packet + ": Lookup returned NULL."); + } } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java index c3720b77..a844cd09 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java @@ -8,6 +8,7 @@ import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +import com.comphenix.protocol.reflect.FieldAccessException; import com.google.common.base.Preconditions; /** @@ -100,9 +101,9 @@ public class PacketInjectorBuilder { *

* Note that any non-null builder parameters must be set. * @return The created injector. - * @throws IllegalAccessException If anything goes wrong in terms of reflection. + * @throws FieldAccessException If anything goes wrong in terms of reflection. */ - public PacketInjector buildInjector() throws IllegalAccessException { + public PacketInjector buildInjector() throws FieldAccessException { initializeDefaults(); return new ProxyPacketInjector(classLoader, invoker, playerInjection, reporter); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java index 918546f4..0cfea2ed 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java @@ -25,10 +25,15 @@ import java.util.Set; import net.sf.cglib.proxy.Factory; +import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.TroveWrapper; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; @@ -39,6 +44,8 @@ import com.google.common.collect.ImmutableSet; */ @SuppressWarnings("rawtypes") public class PacketRegistry { + private static final int MIN_SERVER_PACKETS = 5; + private static final int MIN_CLIENT_PACKETS = 5; // Fuzzy reflection private static FuzzyReflection packetRegistry; @@ -67,6 +74,14 @@ public class PacketRegistry { try { Field packetsField = getPacketRegistry().getFieldByType("packetsField", Map.class); packetToID = (Map) FieldUtils.readStaticField(packetsField, true); + } catch (IllegalArgumentException e) { + // Spigot 1.2.5 MCPC workaround + try { + packetToID = getSpigotWrapper(); + } catch (Exception e2) { + // Very bad indeed + throw new IllegalArgumentException(e.getMessage() + "; Spigot workaround failed.", e2); + } } catch (IllegalAccessException e) { throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e); @@ -76,6 +91,40 @@ public class PacketRegistry { return packetToID; } + private static Map getSpigotWrapper() throws IllegalAccessException { + // If it talks like a duck, etc. + // Perhaps it would be nice to have a proper duck typing library as well + FuzzyClassContract mapLike = FuzzyClassContract.newBuilder(). + method(FuzzyMethodContract.newBuilder(). + nameExact("size").returnTypeExact(int.class)). + method(FuzzyMethodContract.newBuilder(). + nameExact("put").parameterCount(2)). + method(FuzzyMethodContract.newBuilder(). + nameExact("get").parameterCount(1)). + build(); + + Field packetsField = getPacketRegistry().getField( + FuzzyFieldContract.newBuilder().typeMatches(mapLike).build()); + Object troveMap = FieldUtils.readStaticField(packetsField, true); + + // Check for stupid no_entry_values + try { + Field field = FieldUtils.getField(troveMap.getClass(), "no_entry_value", true); + Integer value = (Integer) FieldUtils.readField(field, troveMap, true); + + if (value >= 0 && value < 256) { + // Someone forgot to set the no entry value. Let's help them. + FieldUtils.writeField(field, troveMap, -1); + } + } catch (IllegalArgumentException e) { + // Whatever + ProtocolLibrary.getErrorReporter().reportWarning(PacketRegistry.class, "Unable to correct no entry value.", e); + } + + // We'll assume this a Trove map + return TroveWrapper.getDecoratedMap(troveMap); + } + /** * Retrieve the cached fuzzy reflection instance allowing access to the packet registry. * @return Reflected packet registry. @@ -109,6 +158,10 @@ public class PacketRegistry { */ public static Set getServerPackets() throws FieldAccessException { initializeSets(); + + // Sanity check. This is impossible! + if (serverPackets != null && serverPackets.size() < MIN_SERVER_PACKETS) + throw new FieldAccessException("Server packet list is empty. Seems to be unsupported"); return serverPackets; } @@ -119,6 +172,10 @@ public class PacketRegistry { */ public static Set getClientPackets() throws FieldAccessException { initializeSets(); + + // As above + if (clientPackets != null && clientPackets.size() < MIN_CLIENT_PACKETS) + throw new FieldAccessException("Client packet list is empty. Seems to be unsupported"); return clientPackets; } @@ -140,6 +197,14 @@ public class PacketRegistry { serverPackets = ImmutableSet.copyOf(serverPacketsRef); clientPackets = ImmutableSet.copyOf(clientPacketsRef); + // Check sizes + if (serverPackets.size() < MIN_SERVER_PACKETS) + ProtocolLibrary.getErrorReporter().reportWarning( + PacketRegistry.class, "Too few server packets detected: " + serverPackets.size()); + if (clientPackets.size() < MIN_CLIENT_PACKETS) + ProtocolLibrary.getErrorReporter().reportWarning( + PacketRegistry.class, "Too few client packets detected: " + clientPackets.size()); + } else { throw new FieldAccessException("Cannot retrieve packet client/server sets."); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java index 391c321f..a6744fb0 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java @@ -37,6 +37,7 @@ import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.MethodInfo; @@ -49,6 +50,85 @@ import com.comphenix.protocol.utility.MinecraftReflection; * @author Kristian */ class ProxyPacketInjector implements PacketInjector { + /** + * Represents a way to update the packet ID to class lookup table. + * @author Kristian + */ + private static interface PacketClassLookup { + public void setLookup(int packetID, Class clazz); + } + + private static class IntHashMapLookup implements PacketClassLookup { + // The "put" method that associates a packet ID with a packet class + private Method putMethod; + private Object intHashMap; + + public IntHashMapLookup() throws IllegalAccessException { + initialize(); + } + + @Override + public void setLookup(int packetID, Class clazz) { + try { + putMethod.invoke(intHashMap, packetID, clazz); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Illegal argument.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access method.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Exception occured in IntHashMap.put.", e); + } + } + + private void initialize() throws IllegalAccessException { + if (intHashMap == null) { + // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. + Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true). + getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); + + try { + intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Minecraft is incompatible.", e); + } + + // Now, get the "put" method. + putMethod = FuzzyReflection.fromObject(intHashMap). + getMethodByParameters("put", int.class, Object.class); + } + } + } + + private static class ArrayLookup implements PacketClassLookup { + private Class[] array; + + public ArrayLookup() throws IllegalAccessException { + initialize(); + } + + @Override + public void setLookup(int packetID, Class clazz) { + array[packetID] = clazz; + } + + private void initialize() throws IllegalAccessException { + FuzzyReflection reflection = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()); + + // Is there a Class array with 256 elements instead? + for (Field field : reflection.getFieldListByType(Class[].class)) { + Class[] test = (Class[]) FieldUtils.readField(field, (Object)null); + + if (test.length == 256) { + array = test; + return; + } + } + throw new IllegalArgumentException( + "Unable to find an array with the type " + Class[].class + + " in " + MinecraftReflection.getPacketClass()); + } + } + /** * Matches the readPacketData(DataInputStream) method in Packet. */ @@ -58,9 +138,7 @@ class ProxyPacketInjector implements PacketInjector { parameterCount(1). build(); - // The "put" method that associates a packet ID with a packet class - private static Method putMethod; - private static Object intHashMap; + private static PacketClassLookup lookup; // The packet filter manager private ListenerInvoker manager; @@ -78,7 +156,7 @@ class ProxyPacketInjector implements PacketInjector { private CallbackFilter filter; public ProxyPacketInjector(ClassLoader classLoader, ListenerInvoker manager, - PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException { + PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws FieldAccessException { this.classLoader = classLoader; this.manager = manager; @@ -100,20 +178,21 @@ class ProxyPacketInjector implements PacketInjector { } } - private void initialize() throws IllegalAccessException { - if (intHashMap == null) { - // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. - Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true). - getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); - + private void initialize() throws FieldAccessException { + if (lookup == null) { try { - intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true); - } catch (IllegalArgumentException e) { - throw new RuntimeException("Minecraft is incompatible.", e); + lookup = new IntHashMapLookup(); + } catch (Exception e1) { + + try { + lookup = new ArrayLookup(); + } catch (Exception e2) { + // Wow + throw new FieldAccessException(e1.getMessage() + ". Workaround failed too.", e2); + } } - // Now, get the "put" method. - putMethod = FuzzyReflection.fromObject(intHashMap).getMethodByParameters("put", int.class, Object.class); + // Should work fine now } } @@ -173,21 +252,12 @@ class ProxyPacketInjector implements PacketInjector { // Add a static reference Enhancer.registerStaticCallbacks(proxy, new Callback[] { NoOp.INSTANCE, modifierReadPacket, modifierRest }); - try { - // Override values - previous.put(packetID, old); - registry.put(proxy, packetID); - overwritten.put(packetID, proxy); - putMethod.invoke(intHashMap, packetID, proxy); - return true; - - } catch (IllegalArgumentException e) { - throw new RuntimeException("Illegal argument.", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access method.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Exception occured in IntHashMap.put.", e); - } + // Override values + previous.put(packetID, old); + registry.put(proxy, packetID); + overwritten.put(packetID, proxy); + lookup.setLookup(packetID, proxy); + return true; } @Override @@ -200,25 +270,14 @@ class ProxyPacketInjector implements PacketInjector { Map previous = PacketRegistry.getPreviousPackets(); Map overwritten = PacketRegistry.getOverwrittenPackets(); - // Use the old class definition - try { - Class old = previous.get(packetID); - Class proxy = PacketRegistry.getPacketClassFromID(packetID); - - putMethod.invoke(intHashMap, packetID, old); - previous.remove(packetID); - registry.remove(proxy); - overwritten.remove(packetID); - return true; - - // Handle some problems - } catch (IllegalArgumentException e) { - return false; - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access method.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Exception occured in IntHashMap.put.", e); - } + Class old = previous.get(packetID); + Class proxy = PacketRegistry.getPacketClassFromID(packetID); + + lookup.setLookup(packetID, old); + previous.remove(packetID); + registry.remove(proxy); + overwritten.remove(packetID); + return true; } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java new file mode 100644 index 00000000..bef4b369 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/TroveWrapper.java @@ -0,0 +1,104 @@ +package com.comphenix.protocol.wrappers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; + +/** + * Wrap a GNU Trove Collection class with an equivalent Java Collection class. + * @author Kristian + */ +public class TroveWrapper { + private volatile static Class decorators; + + /** + * Retrieve a Java wrapper for the corresponding Trove map. + * @param troveMap - the trove map to wrap. + * @return The wrapped GNU Trove map. + * @throws IllegalStateException If GNU Trove cannot be found in the class map. + * @throws IllegalArgumentException If troveMap is NULL. + * @throws FieldAccessException Error in wrapper method or lack of reflection permissions. + */ + public static Map getDecoratedMap(@Nonnull Object troveMap) { + @SuppressWarnings("unchecked") + Map result = (Map) getDecorated(troveMap); + return result; + } + + /** + * Retrieve a Java wrapper for the corresponding Trove set. + * @param troveSet - the trove set to wrap. + * @return The wrapped GNU Trove set. + * @throws IllegalStateException If GNU Trove cannot be found in the class map. + * @throws IllegalArgumentException If troveSet is NULL. + * @throws FieldAccessException Error in wrapper method or lack of reflection permissions. + */ + public static Set getDecoratedSet(@Nonnull Object troveSet) { + @SuppressWarnings("unchecked") + Set result = (Set) getDecorated(troveSet); + return result; + } + + /** + * Retrieve a Java wrapper for the corresponding Trove list. + * @param troveList - the trove list to wrap. + * @return The wrapped GNU Trove list. + * @throws IllegalStateException If GNU Trove cannot be found in the class map. + * @throws IllegalArgumentException If troveList is NULL. + * @throws FieldAccessException Error in wrapper method or lack of reflection permissions. + */ + public static List getDecoratedList(@Nonnull Object troveList) { + @SuppressWarnings("unchecked") + List result = (List) getDecorated(troveList); + return result; + } + + private static Object getDecorated(@Nonnull Object trove) { + if (trove == null) + throw new IllegalArgumentException("trove instance cannot be non-null."); + + AbstractFuzzyMatcher> match = FuzzyMatchers.matchSuper(trove.getClass()); + + if (decorators == null) { + try { + // Attempt to get decorator class + decorators = TroveWrapper.class.getClassLoader().loadClass("gnu.trove.TDecorators"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Cannot find TDecorators in Gnu Trove.", e); + } + } + + // Find an appropriate wrapper method in TDecorators + for (Method method : decorators.getMethods()) { + Class[] types = method.getParameterTypes(); + + if (types.length == 1 && match.isMatch(types[0], null)) { + try { + Object result = method.invoke(null, trove); + + if (result == null) + throw new FieldAccessException("Wrapper returned NULL."); + else + return result; + + } catch (IllegalArgumentException e) { + throw new FieldAccessException("Cannot invoke wrapper method.", e); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Illegal access.", e); + } catch (InvocationTargetException e) { + throw new FieldAccessException("Error in invocation.", e); + } + } + } + + throw new IllegalArgumentException("Cannot find decorator for " + trove + " (" + trove.getClass() + ")"); + } +} From da7a58fb43c833650d56c2b823729f21b81722d2 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 9 Apr 2013 15:11:43 +0200 Subject: [PATCH 20/46] Incrementing to 2.4.0. This is due to the non-breaking API changes in 505226f8adb08820b938705b160ddf0e54ae0286 --- ProtocolLib/pom.xml | 2 +- ProtocolLib/src/main/resources/plugin.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 57a59bcd..ed0eca1b 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.3.1-SNAPSHOT + 2.4.0 jar Provides read/write access to the Minecraft protocol. diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 1b2a0377..dc9da164 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.3.1-SNAPSHOT +version: 2.4.0 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib From 72172805bae959d4bdbc9e158a2b586fe169dd13 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 9 Apr 2013 16:48:26 +0200 Subject: [PATCH 21/46] Exploit the internal JavaScript parser to determine if the exp is done. The original code attempted to parse the JavaScript as it went along, counting open and close brackets. Unfortunately, this doesn't take comments and string literals into consideration, so it would very likely have failed with more complicated filters. Instead, we'll let the JavaScript compiler handle all the complexity and simply see if the code compiles. If it doesn't, but the error occured in the last line, we assume it can be recovered by adding a new line. --- .../com/comphenix/protocol/CommandFilter.java | 132 +++++++++--------- .../protocol/MultipleLinesPrompt.java | 110 +++++++++++++-- 2 files changed, 165 insertions(+), 77 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index 0ff5a162..0afe058b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -2,6 +2,7 @@ package com.comphenix.protocol; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; @@ -16,11 +17,11 @@ import org.bukkit.conversations.Conversable; import org.bukkit.conversations.Conversation; import org.bukkit.conversations.ConversationAbandonedEvent; import org.bukkit.conversations.ConversationAbandonedListener; -import org.bukkit.conversations.ConversationCanceller; import org.bukkit.conversations.ConversationContext; import org.bukkit.conversations.ConversationFactory; import org.bukkit.plugin.Plugin; +import com.comphenix.protocol.MultipleLinesPrompt.MultipleConversationCanceller; import com.comphenix.protocol.concurrency.IntegerSet; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketEvent; @@ -34,23 +35,17 @@ import com.google.common.collect.Ranges; * @author Kristian */ public class CommandFilter extends CommandBase { - @SuppressWarnings("serial") - public static class FilterFailedException extends RuntimeException { - private Filter filter; - - public FilterFailedException() { - super(); - } - - public FilterFailedException(String message, Filter filter, Throwable cause) { - super(message, cause); - this.filter = filter; - } - - public Filter getFilter() { - return filter; - } + public interface FilterFailedHandler{ + /** + * Invoked when a given filter has failed. + * @param event - the packet event. + * @param filter - the filter that failed. + * @param ex - the failure. + * @returns TRUE to keep processing this filter, FALSE to remove it. + */ + public boolean handle(PacketEvent event, Filter filter, Exception ex); } + /** * Possible sub commands. * @@ -123,7 +118,7 @@ public class CommandFilter extends CommandBase { * @param context - the current script context. * @param event - the packet event to evaluate. * @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise. - * @throws ScriptException If the compilation failed. + * @throws ScriptException If the compilation failed or the filter is not valid. */ public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException { if (!isApplicable(event)) @@ -132,7 +127,13 @@ public class CommandFilter extends CommandBase { compile(context); try { - return (Boolean) ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle()); + Object result = ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle()); + + if (result instanceof Boolean) + return (Boolean) result; + else + throw new ScriptException("Filter result wasn't a boolean: " + result); + } catch (NoSuchMethodException e) { // Must be a fault with the script engine itself throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e); @@ -159,54 +160,36 @@ public class CommandFilter extends CommandBase { } } - private static class BracketBalance implements ConversationCanceller { - private String KEY_BRACKET_COUNT = "bracket_balance.count"; - - // What to set the initial counter - private final int initialBalance; - - public BracketBalance(int initialBalance) { - this.initialBalance = initialBalance; - } - + private class CompilationSuccessCanceller implements MultipleConversationCanceller { @Override public boolean cancelBasedOnInput(ConversationContext context, String in) { - Object stored = context.getSessionData(KEY_BRACKET_COUNT); - int value = 0; - - // Get the stored value - if (stored instanceof Integer) { - value = (Integer)stored; - } else { - value = initialBalance; - } - - value += count(in, '{') - count(in, '}'); - context.setSessionData(KEY_BRACKET_COUNT, value); - - // Cancel if the bracket balance is zero - return value <= 0; - } - - private int count(String text, char character) { - int counter = 0; - - for (int i=0; i < text.length(); i++) { - if (text.charAt(i) == character) { - counter++; - } - } - return counter; + throw new UnsupportedOperationException("Cannot cancel on the last line alone."); } @Override public void setConversation(Conversation conversation) { - // Whatever + // Ignore + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine, StringBuilder lines, int lineCount) { + try { + engine.eval("function(event, packet) {\n" + lines.toString()); + + // It compiles - accept the filter! + return true; + } catch (ScriptException e) { + // We also have the function() line + int realLineCount = lineCount + 1; + + // Only possible to recover from an error on the last line. + return e.getLineNumber() < realLineCount; + } } @Override - public ConversationCanceller clone() { - return new BracketBalance(initialBalance); + public CompilationSuccessCanceller clone() { + return new CompilationSuccessCanceller(); } } @@ -251,18 +234,41 @@ public class CommandFilter extends CommandBase { /** * Determine whether or not to pass the given packet event to the packet listeners. + *

+ * Uses a default filter failure handler that simply prints the error message and removes the filter. * @param event - the event. * @return TRUE if we should, FALSE otherwise. + */ + public boolean filterEvent(PacketEvent event) { + return filterEvent(event, new FilterFailedHandler() { + @Override + public boolean handle(PacketEvent event, Filter filter, Exception ex) { + reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event); + reporter.reportWarning(this, "Removing filter " + filter.getName() + " for causing an exception."); + return false; + } + }); + } + + /** + * Determine whether or not to pass the given packet event to the packet listeners. + * @param event - the event. + * @param handler - failure handler. + * @return TRUE if we should, FALSE otherwise. * @throws FilterFailedException If one of the filters failed. */ - public boolean filterEvent(PacketEvent event) throws FilterFailedException { - for (Filter filter : filters) { + public boolean filterEvent(PacketEvent event, FilterFailedHandler handler) { + for (Iterator it = filters.iterator(); it.hasNext(); ) { + Filter filter = it.next(); + try { if (!filter.evaluate(engine, event)) { return false; } - } catch (ScriptException e) { - throw new FilterFailedException("Filter failed.", filter, e); + } catch (Exception ex) { + if (!handler.handle(event, filter, ex)) { + it.remove(); + } } } // Pass! @@ -297,7 +303,7 @@ public class CommandFilter extends CommandBase { // Make sure we can use the conversable interface if (sender instanceof Conversable) { final MultipleLinesPrompt prompt = - new MultipleLinesPrompt(new BracketBalance(1), "function(event, packet) {"); + new MultipleLinesPrompt(new CompilationSuccessCanceller(), "function(event, packet) {"); new ConversationFactory(plugin). withFirstPrompt(prompt). diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java index 48695a60..53f1229e 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java @@ -1,5 +1,6 @@ package com.comphenix.protocol; +import org.bukkit.conversations.Conversation; import org.bukkit.conversations.ConversationCanceller; import org.bukkit.conversations.ConversationContext; import org.bukkit.conversations.ExactMatchConversationCanceller; @@ -12,40 +13,117 @@ import org.bukkit.conversations.StringPrompt; * @author Kristian */ class MultipleLinesPrompt extends StringPrompt { + /** + * Represents a canceller that determines if the multiple lines prompt is finished. + * @author Kristian + */ + public static interface MultipleConversationCanceller extends ConversationCanceller { + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine); + + /** + * Determine if the current prompt is done based on the context, last + * line and collected lines. + * + * @param context - current context. + * @param currentLine - current (last) line. + * @param lines - collected lines. + * @param lineCount - number of lines. + * @return TRUE if we are done, FALSE otherwise. + */ + public boolean cancelBasedOnInput(ConversationContext context, String currentLine, + StringBuilder lines, int lineCount); + } + + /** + * A wrapper class for turning a ConversationCanceller into a MultipleConversationCanceller. + * @author Kristian + */ + private static class MultipleWrapper implements MultipleConversationCanceller { + private ConversationCanceller canceller; + + public MultipleWrapper(ConversationCanceller canceller) { + this.canceller = canceller; + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine) { + return canceller.cancelBasedOnInput(context, currentLine); + } + + @Override + public boolean cancelBasedOnInput(ConversationContext context, String currentLine, + StringBuilder lines, int lineCount) { + return cancelBasedOnInput(context, currentLine); + } + + @Override + public void setConversation(Conversation conversation) { + canceller.setConversation(conversation); + } + + @Override + public MultipleWrapper clone() { + return new MultipleWrapper(canceller.clone()); + } + } + // Feels a bit like Android private static final String KEY = "multiple_lines_prompt"; private static final String KEY_LAST = KEY + ".last_line"; - - private final ConversationCanceller endMarker; + private static final String KEY_LINES = KEY + ".linecount"; + + private final MultipleConversationCanceller endMarker; private final String initialPrompt; - + /** * Retrieve and remove the current accumulated input. - * @param context - conversation context. + * + * @param context + * - conversation context. * @return The accumulated input, or NULL if not found. */ public String removeAccumulatedInput(ConversationContext context) { Object result = context.getSessionData(KEY); - + if (result instanceof StringBuilder) { context.setSessionData(KEY, null); + context.setSessionData(KEY_LINES, null); return ((StringBuilder) result).toString(); } else { return null; } } - + /** - * Construct a multiple lines input prompt with a specific end marker. + * Construct a multiple lines input prompt with a specific end marker. *

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

+ * Note: Use {@link #MultipleLinesPrompt(MultipleConversationCanceller, String)} if implementing a custom canceller. + * @param endMarker - the end marker. + * @param initialPrompt - the initial prompt text. + */ public MultipleLinesPrompt(ConversationCanceller endMarker, String initialPrompt) { + this.endMarker = new MultipleWrapper(endMarker); + this.initialPrompt = initialPrompt; + } + + /** + * Construct a multiple lines input prompt with a specific end marker implementation. + * @param endMarker - the end marker. + * @param initialPrompt - the initial prompt text. + */ + public MultipleLinesPrompt(MultipleConversationCanceller endMarker, String initialPrompt) { this.endMarker = endMarker; this.initialPrompt = initialPrompt; } @@ -53,17 +131,21 @@ class MultipleLinesPrompt extends StringPrompt { @Override public Prompt acceptInput(ConversationContext context, String in) { StringBuilder result = (StringBuilder) context.getSessionData(KEY); + Integer count = (Integer) context.getSessionData(KEY_LINES); - if (result == null) { + // Handle first run + if (result == null) context.setSessionData(KEY, result = new StringBuilder()); - } + if (count == null) + count = 0; // Save the last line as well context.setSessionData(KEY_LAST, in); - result.append(in); - + context.setSessionData(KEY_LINES, ++count); + result.append(in + "\n"); + // And we're done - if (endMarker.cancelBasedOnInput(context, in)) + if (endMarker.cancelBasedOnInput(context, in, result, count)) return Prompt.END_OF_CONVERSATION; else return this; @@ -72,7 +154,7 @@ class MultipleLinesPrompt extends StringPrompt { @Override public String getPromptText(ConversationContext context) { Object last = context.getSessionData(KEY_LAST); - + if (last instanceof String) return (String) last; else From e7be3be17f03fa8519c027b3332fe5f407d8d1c9 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 9 Apr 2013 16:49:17 +0200 Subject: [PATCH 22/46] Increment to 2.4.1 --- ProtocolLib/pom.xml | 2 +- ProtocolLib/src/main/resources/plugin.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index ed0eca1b..64d83d26 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.4.0 + 2.4.1 jar Provides read/write access to the Minecraft protocol. diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index dc9da164..93465576 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.4.0 +version: 2.4.1 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib From 95603d3fa23613f2bb23775cf47bd255875746ca Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Thu, 11 Apr 2013 22:51:20 +0200 Subject: [PATCH 23/46] Increment to 2.4.2-SNAPSHOT --- ProtocolLib/pom.xml | 4 ++-- ProtocolLib/src/main/resources/plugin.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 64d83d26..4adca81e 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.4.1 + 2.4.2-SNAPSHOT jar Provides read/write access to the Minecraft protocol. @@ -203,7 +203,7 @@ org.bukkit craftbukkit - 1.4.7-R0.1 + 1.5.1-R0.2-SNAPSHOT provided diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 93465576..33f25d79 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.4.1 +version: 2.4.2-SNAPSHOT description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib From bda4474d62fab6c380569f3e04a703002b336487 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Thu, 11 Apr 2013 22:51:34 +0200 Subject: [PATCH 24/46] Update packets to 1.5.1 --- ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java index ada35964..8b543e32 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java @@ -114,6 +114,10 @@ public final class Packets { public static final int PLAYER_INFO = 201; public static final int ABILITIES = 202; public static final int TAB_COMPLETE = 203; + public static final int SCOREBOARD_OBJECTIVE = 206; + public static final int UPDATE_SCORE = 207; + public static final int DISPLAY_SCOREBOARD = 208; + public static final int TEAMS = 209; public static final int CUSTOM_PAYLOAD = 250; public static final int KEY_RESPONSE = 252; public static final int KEY_REQUEST = 253; From 8e11a406628c9280cd994d7bd329a0346558c775 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Thu, 11 Apr 2013 22:51:44 +0200 Subject: [PATCH 25/46] Update unit test to 1.5.1 --- .../protocol/reflect/instances/DefaultInstances.java | 1 + .../com/comphenix/protocol/BukkitInitialization.java | 12 ++++++++++-- .../protocol/events/PacketContainerTest.java | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java index 826a4ecd..faaf84fc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -327,6 +327,7 @@ public class DefaultInstances implements InstanceProvider { try { return (T) constructor.newInstance(params); } catch (Exception e) { + e.printStackTrace(); // Cannot create it return null; } diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java b/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java index 9fac9cac..9012e8a8 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java @@ -4,8 +4,10 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import net.minecraft.server.StatisticList; + // Will have to be updated for every version though -import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemFactory; +import org.bukkit.craftbukkit.inventory.CraftItemFactory; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -34,6 +36,12 @@ public class BukkitInitialization { initializePackage(); + try { + StatisticList.b(); + } catch (Exception e) { + // Swallow + } + // Mock the server object Server mockedServer = mock(Server.class); ItemFactory mockedFactory = mock(CraftItemFactory.class); @@ -55,6 +63,6 @@ public class BukkitInitialization { */ public static void initializePackage() { // Initialize reflection - MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_4_R1", "org.bukkit.craftbukkit.v1_4_R1"); + MinecraftReflection.setMinecraftPackage("net.minecraft.server", "org.bukkit.craftbukkit"); } } diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index 1b66511e..f9a7c891 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -22,7 +22,7 @@ import java.lang.reflect.Array; import java.util.List; // Will have to be updated for every version though -import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemFactory; +import org.bukkit.craftbukkit.inventory.CraftItemFactory; import org.bukkit.Material; import org.bukkit.WorldType; From 2411d29822dde11816e78549b16c34408bfecefb Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Fri, 12 Apr 2013 05:09:05 +0200 Subject: [PATCH 26/46] Correctly reference v1_5_R2. --- .../java/com/comphenix/protocol/BukkitInitialization.java | 6 +++--- .../com/comphenix/protocol/events/PacketContainerTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java b/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java index 9012e8a8..39b6b55d 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/BukkitInitialization.java @@ -4,10 +4,10 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import net.minecraft.server.StatisticList; +import net.minecraft.server.v1_5_R2.StatisticList; // Will have to be updated for every version though -import org.bukkit.craftbukkit.inventory.CraftItemFactory; +import org.bukkit.craftbukkit.v1_5_R2.inventory.CraftItemFactory; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -63,6 +63,6 @@ public class BukkitInitialization { */ public static void initializePackage() { // Initialize reflection - MinecraftReflection.setMinecraftPackage("net.minecraft.server", "org.bukkit.craftbukkit"); + MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_5_R2", "org.bukkit.craftbukkit.v1_5_R2"); } } diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index f9a7c891..d305adf4 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -22,7 +22,7 @@ import java.lang.reflect.Array; import java.util.List; // Will have to be updated for every version though -import org.bukkit.craftbukkit.inventory.CraftItemFactory; +import org.bukkit.craftbukkit.v1_5_R2.inventory.CraftItemFactory; import org.bukkit.Material; import org.bukkit.WorldType; From 8a5ebb88e0b03a135f9c84951672437e467bad27 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Fri, 12 Apr 2013 17:12:42 +0200 Subject: [PATCH 27/46] Added the ability to recover from engine incompatibilities. --- .../com/comphenix/protocol/CommandFilter.java | 42 +++++++++++++++---- .../comphenix/protocol/ProtocolConfig.java | 21 +++++++++- ProtocolLib/src/main/resources/config.yml | 5 ++- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index 0afe058b..cb8adccc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -220,16 +220,44 @@ public class CommandFilter extends CommandBase { } private void initalizeScript() { + try { + // First attempt + initializeEngine(); + } catch (ScriptException e1) { + // It's not a huge deal + printPackageWarning(e1); + + if (!config.getScriptEngineName().equals("rhino")) { + reporter.reportWarning(this, "Falling back to the Rhino engine."); + config.setScriptEngineName("rhino"); + config.saveAll(); + + try { + initializeEngine(); + } catch (ScriptException e2) { + // And again .. + printPackageWarning(e2); + } + } + } + } + + private void printPackageWarning(ScriptException e) { + reporter.reportWarning(this, "Unable to initialize packages for JavaScript engine.", e); + } + + /** + * Initialize the current configured engine. + * @throws ScriptException If we are unable to import packages. + */ + private void initializeEngine() throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); - engine = manager.getEngineByName("JavaScript"); + engine = manager.getEngineByName(config.getScriptEngineName()); // Import useful packages - try { - engine.eval("importPackage(org.bukkit);"); - engine.eval("importPackage(com.comphenix.protocol.reflect);"); - } catch (ScriptException e) { - throw new IllegalStateException("Unable to initialize packages for JavaScript engine.", e); - } + engine.eval("importPackage(org.bukkit);"); + engine.eval("importPackage(com.comphenix.protocol.reflect);"); + } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index 4ce0bf06..d6571e85 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -41,9 +41,10 @@ class ProtocolConfig { private static final String BACKGROUND_COMPILER_ENABLED = "background compiler"; private static final String DEBUG_MODE_ENABLED = "debug"; - private static final String INJECTION_METHOD = "injection method"; + private static final String SCRIPT_ENGINE_NAME = "script engine"; + private static final String UPDATER_NOTIFY = "notify"; private static final String UPDATER_DOWNLAD = "download"; private static final String UPDATER_DELAY = "delay"; @@ -257,6 +258,24 @@ class ProtocolConfig { updater.set(UPDATER_LAST_TIME, lastTimeSeconds); } + /** + * Retrieve the unique name of the script engine to use for filtering. + * @return Script engine to use. + */ + public String getScriptEngineName() { + return global.getString(SCRIPT_ENGINE_NAME, "JavaScript"); + } + + /** + * Set the unique name of the script engine to use for filtering. + *

+ * This setting will take effect next time ProtocolLib is started. + * @param name - name of the script engine to use. + */ + public void setScriptEngineName(String name) { + global.set(SCRIPT_ENGINE_NAME, name); + } + /** * Retrieve the default injection method. * @return Default method. diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index e9185886..1de87503 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -21,4 +21,7 @@ global: injection method: # Whether or not to enable the filter command - debug: false \ No newline at end of file + debug: false + + # The engine used by the filter command + script engine: JavaScript \ No newline at end of file From cd78e311d59ce6418b0ae9bb4c062a1e46e53abb Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sat, 13 Apr 2013 14:20:35 +0200 Subject: [PATCH 28/46] Looks like Rhino is not guaranteed after all. Add some checks. --- .../com/comphenix/protocol/CommandFilter.java | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java index cb8adccc..2a249286 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java @@ -198,6 +198,9 @@ public class CommandFilter extends CommandBase { */ public static final String NAME = "filter"; + // Default error handler + private FilterFailedHandler defaultFailedHandler; + // Currently registered filters private List filters = new ArrayList(); @@ -223,6 +226,11 @@ public class CommandFilter extends CommandBase { try { // First attempt initializeEngine(); + + // Oh for .. + if (!isInitialized()) { + throw new ScriptException("A JavaScript engine could not be found."); + } } catch (ScriptException e1) { // It's not a huge deal printPackageWarning(e1); @@ -234,6 +242,10 @@ public class CommandFilter extends CommandBase { try { initializeEngine(); + + if (!isInitialized()) { + reporter.reportWarning(this, "Could not load Rhino either. Please upgrade your JVM or OS."); + } } catch (ScriptException e2) { // And again .. printPackageWarning(e2); @@ -255,11 +267,35 @@ public class CommandFilter extends CommandBase { engine = manager.getEngineByName(config.getScriptEngineName()); // Import useful packages - engine.eval("importPackage(org.bukkit);"); - engine.eval("importPackage(com.comphenix.protocol.reflect);"); - + if (engine != null) { + engine.eval("importPackage(org.bukkit);"); + engine.eval("importPackage(com.comphenix.protocol.reflect);"); + } + } + + /** + * Determine if the filter engine has been successfully initialized. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean isInitialized() { + return engine != null; } + private FilterFailedHandler getDefaultErrorHandler() { + // No need to create a new object every time + if (defaultFailedHandler == null) { + defaultFailedHandler = new FilterFailedHandler() { + @Override + public boolean handle(PacketEvent event, Filter filter, Exception ex) { + reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event); + reporter.reportWarning(this, "Removing filter " + filter.getName() + " for causing an exception."); + return false; + } + }; + } + return defaultFailedHandler; + } + /** * Determine whether or not to pass the given packet event to the packet listeners. *

@@ -268,14 +304,7 @@ public class CommandFilter extends CommandBase { * @return TRUE if we should, FALSE otherwise. */ public boolean filterEvent(PacketEvent event) { - return filterEvent(event, new FilterFailedHandler() { - @Override - public boolean handle(PacketEvent event, Filter filter, Exception ex) { - reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event); - reporter.reportWarning(this, "Removing filter " + filter.getName() + " for causing an exception."); - return false; - } - }); + return filterEvent(event, getDefaultErrorHandler()); } /** @@ -313,6 +342,10 @@ public class CommandFilter extends CommandBase { sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!"); return true; } + if (!isInitialized()) { + sender.sendMessage(ChatColor.RED + "JavaScript engine was not present. Filter system is disabled."); + return true; + } final SubCommand command = parseCommand(args, 0); final String name = args[1]; From 37dd46432a9abc5dda533f61e80afbcd41a0b6d4 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sat, 13 Apr 2013 15:27:31 +0200 Subject: [PATCH 29/46] Ignore these errors again. --- .../comphenix/protocol/reflect/instances/DefaultInstances.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java index faaf84fc..f5fa9e54 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -327,7 +327,7 @@ public class DefaultInstances implements InstanceProvider { try { return (T) constructor.newInstance(params); } catch (Exception e) { - e.printStackTrace(); + //e.printStackTrace(); // Cannot create it return null; } From 5ca29ef5ceb9a4f0e5f6d2a0b96cd62e0116b10b Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 14 Apr 2013 03:53:27 +0200 Subject: [PATCH 30/46] Leave ThreadDead alone! The original intent of catching throwable is to "sandbox" arbitrary plugin logic and prevent it from ever accidentally killing threads on the server. A LinkageError due to a missing or old dependency shouldn't bring down the server, so we secure it by catching all exceptions around plugin event handlers. Trouble is, this also catches exceptions such as OutOfMemoryError or ThreadDead, which assuredly should NOT be caught. The latter case has even occured in the wild as seen by ticket 45 of TagAPI on BukkitDev. Minecraft may terminate the reader and writer thread by calling stop(), and this could occur within the event handler in a plugin. So we should let ThreadDead go and propagate it to the appropriate handler in Minecraft. --- .../injector/SortedPacketListenerList.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java index e1e3d912..5391a948 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -52,6 +52,11 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu try { event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketReceiving(event); + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; } catch (Throwable e) { // Minecraft doesn't want your Exception. reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e, @@ -78,6 +83,11 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketReceiving(event); } + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; } catch (Throwable e) { // Minecraft doesn't want your Exception. reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e, @@ -101,6 +111,11 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu try { event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketSending(event); + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; } catch (Throwable e) { // Minecraft doesn't want your Exception. reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e, @@ -127,6 +142,11 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR); element.getListener().onPacketSending(event); } + + } catch (OutOfMemoryError e) { + throw e; + } catch (ThreadDeath e) { + throw e; } catch (Throwable e) { // Minecraft doesn't want your Exception. reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e, From b451a5a67259a38ddb10beb85d740b98488ec3bb Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Fri, 19 Apr 2013 21:34:34 +0200 Subject: [PATCH 31/46] Adding a plugin verification system detecting common programmer errors Initially we will detect plugins that attempt to register a listener in ProtocolLib without setting "depend" or "soft-depend". --- .../comphenix/protocol/ProtocolLibrary.java | 2 +- .../injector/PacketFilterManager.java | 30 ++- .../protocol/injector/PluginVerifier.java | 211 ++++++++++++++++++ 3 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index d1e27b6f..3071707f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -147,7 +147,7 @@ public class ProtocolLibrary extends JavaPlugin { unhookTask = new DelayedSingleTask(this); protocolManager = new PacketFilterManager( - getClassLoader(), getServer(), version, unhookTask, detailedReporter); + getClassLoader(), getServer(), this, version, unhookTask, detailedReporter); // Setup error reporter detailedReporter.addGlobalParameter("manager", protocolManager); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index af7453cc..6aa6300b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -148,17 +148,22 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Spigot listener, if in use private SpigotPacketInjector spigotInjector; + // Plugin verifier + private PluginVerifier pluginVerifier; + /** * Only create instances of this class if protocol lib is disabled. */ - public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) { - this(classLoader, server, new MinecraftVersion(server), unhookTask, reporter); + public PacketFilterManager(ClassLoader classLoader, Server server, Plugin library, DelayedSingleTask unhookTask, ErrorReporter reporter) { + this(classLoader, server, library, new MinecraftVersion(server), unhookTask, reporter); } /** * Only create instances of this class if protocol lib is disabled. */ - public PacketFilterManager(ClassLoader classLoader, Server server, MinecraftVersion mcVersion, DelayedSingleTask unhookTask, ErrorReporter reporter) { + public PacketFilterManager(ClassLoader classLoader, Server server, Plugin library, + MinecraftVersion mcVersion, DelayedSingleTask unhookTask, ErrorReporter reporter) { + if (reporter == null) throw new IllegalArgumentException("reporter cannot be NULL."); if (classLoader == null) @@ -177,6 +182,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok this.classLoader = classLoader; this.reporter = reporter; + // The plugin verifier + this.pluginVerifier = new PluginVerifier(library); + // Used to determine if injection is needed Predicate isInjectionNecessary = new Predicate() { @Override @@ -267,6 +275,20 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok return ImmutableSet.copyOf(packetListeners); } + /** + * Warn of common programming mistakes. + * @param plugin - plugin to check. + */ + private void printPluginWarnings(Plugin plugin) { + switch (pluginVerifier.verify(plugin)) { + case NO_DEPEND: + reporter.reportWarning(this, plugin + " doesn't depend on ProtocolLib. Check that its plugin.yml has a 'depend' directive."); + case VALID: + // Do nothing + break; + } + } + @Override public void addPacketListener(PacketListener listener) { if (listener == null) @@ -275,6 +297,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // A listener can only be added once if (packetListeners.contains(listener)) return; + // Check plugin + printPluginWarnings(listener.getPlugin()); ListeningWhitelist sending = listener.getSendingWhitelist(); ListeningWhitelist receiving = listener.getReceivingWhitelist(); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java new file mode 100644 index 00000000..9be75b1b --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java @@ -0,0 +1,211 @@ +package com.comphenix.protocol.injector; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginLoadOrder; + +import com.google.common.collect.Sets; + +/** + * Determine if a plugin using ProtocolLib is correct. + * + * @author Kristian + */ +class PluginVerifier { + /** + * A named plugin cannot be found. + * @author Kristian + */ + public static class PluginNotFoundException extends RuntimeException { + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = 8956699101336877611L; + + public PluginNotFoundException() { + super(); + } + + public PluginNotFoundException(String message) { + super(message); + } + } + + public enum VerificationResult { + VALID, + + /** + * The plugin doesn't depend on ProtocolLib directly or indirectly. + */ + NO_DEPEND; + + /** + * Determine if the verification was valid. + */ + public boolean isValid() { + return this == VALID; + } + } + + /** + * Set of plugins that have been loaded after ProtocolLib. + */ + private final Set loadedAfter = new HashSet(); + + /** + * Reference to ProtocolLib. + */ + private final Plugin dependency; + + /** + * Construct a new plugin verifier. + * @param dependency - reference to ProtocolLib, a dependency we require of plugins. + */ + public PluginVerifier(Plugin dependency) { + if (dependency == null) + throw new IllegalArgumentException("dependency cannot be NULL."); + // This would screw with the assumption in hasDependency(Plugin, Plugin) + if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0) + throw new IllegalArgumentException("dependency cannot have a load directives."); + + this.dependency = dependency; + } + + /** + * Retrieve a plugin by name. + * @param pluginName - the non-null name of the plugin to retrieve. + * @return The retrieved plugin. + * @throws PluginNotFoundException If a plugin with the given name cannot be found. + */ + private Plugin getPlugin(String pluginName) { + Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName); + + // Ensure that the plugin exists + if (plugin != null) + return plugin; + else + throw new PluginNotFoundException("Cannot find plugin " + pluginName); + } + + /** + * Performs simple verifications on the given plugin. + *

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

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

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

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

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

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

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

- * Typical conversions include: - *

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

+ * Typical conversions include: + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * WARNING: Do NOT call this method in the main thread. Compiling may easily take 10 ms, which is already + * over 1/4 of a tick (50 ms). Let the background thread automatically compile the structure modifiers instead. + * @param source - structure modifier to compile. + * @return A compiled structure modifier. + */ + @SuppressWarnings("unchecked") + public synchronized StructureModifier compile(StructureModifier source) { + + // We cannot optimize a structure modifier with no public fields + if (!isAnyPublic(source.getFields())) { + return source; + } + + StructureKey key = new StructureKey(source); + Class compiledClass = compiledCache.get(key); + + if (!compiledCache.containsKey(key)) { + compiledClass = generateClass(source); + compiledCache.put(key, compiledClass); + } + + // Next, create an instance of this class + try { + return (StructureModifier) compiledClass.getConstructor( + StructureModifier.class, StructureCompiler.class). + newInstance(source, this); + } catch (OutOfMemoryError e) { + // Print the number of generated classes by the current instances + ProtocolLibrary.getErrorReporter().reportWarning( + this, Report.newBuilder(REPORT_TOO_MANY_GENERATED_CLASSES).messageParam(compiledCache.size()) + ); + throw e; + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Used invalid parameters in instance creation", e); + } catch (SecurityException e) { + throw new RuntimeException("Security limitation!", e); + } catch (InstantiationException e) { + throw new RuntimeException("Error occured while instancing generated class.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security limitation! Cannot create instance of dynamic class.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured while instancing generated class.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Cannot happen.", e); + } + } + + /** + * Retrieve a variable identifier that can uniquely represent the given type. + * @param type - a type. + * @return A unique and legal identifier for the given type. + */ + private String getSafeTypeName(Class type) { + return type.getCanonicalName().replace("[]", "Array").replace(".", "_"); + } + + /** + * Retrieve the compiled name of a given structure modifier. + * @param source - the structure modifier. + * @return The unique, compiled name of a compiled structure modifier. + */ + private String getCompiledName(StructureModifier source) { + Class targetType = source.getTargetType(); + + // Concat class and field type + return "CompiledStructure$" + + getSafeTypeName(targetType) + "$" + + getSafeTypeName(source.getFieldType()); + } + + /** + * Compile a structure modifier. + * @param source - structure modifier. + * @return The compiled structure modifier. + */ + private Class generateClass(StructureModifier source) { + + ClassWriter cw = new ClassWriter(0); + Class targetType = source.getTargetType(); + + String className = getCompiledName(source); + String targetSignature = Type.getDescriptor(targetType); + String targetName = targetType.getName().replace('.', '/'); + + // Define class + cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className, + null, COMPILED_CLASS, null); + + createFields(cw, targetSignature); + createConstructor(cw, className, targetSignature, targetName); + createReadMethod(cw, className, source.getFields(), targetSignature, targetName); + createWriteMethod(cw, className, source.getFields(), targetSignature, targetName); + cw.visitEnd(); + + byte[] data = cw.toByteArray(); + + // Call the define method + try { + if (defineMethod == null) { + Method defined = ClassLoader.class.getDeclaredMethod("defineClass", + new Class[] { String.class, byte[].class, int.class, int.class }); + + // Awesome. Now, create and return it. + defined.setAccessible(true); + defineMethod = defined; + } + + @SuppressWarnings("rawtypes") + Class clazz = (Class) defineMethod.invoke(loader, null, data, 0, data.length); + + // DEBUG CODE: Print the content of the generated class. + //org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(data); + //cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0); + + return clazz; + + } catch (SecurityException e) { + throw new RuntimeException("Cannot use reflection to dynamically load a class.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Incompatible JVM.", e); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Cannot call defineMethod - wrong JVM?", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security limitation! Cannot dynamically load class.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured in code generator.", e); + } + } + + /** + * Determine if at least one of the given fields is public. + * @param fields - field to test. + * @return TRUE if one or more field is publically accessible, FALSE otherwise. + */ + private boolean isAnyPublic(List fields) { + // Are any of the fields public? + for (int i = 0; i < fields.size(); i++) { + if (isPublic(fields.get(i))) { + return true; + } + } + + return false; + } + + private boolean isPublic(Field field) { + return Modifier.isPublic(field.getModifiers()); + } + + private boolean isNonFinal(Field field) { + return !Modifier.isFinal(field.getModifiers()); + } + + private void createFields(ClassWriter cw, String targetSignature) { + FieldVisitor typedField = cw.visitField(Opcodes.ACC_PRIVATE, "typedTarget", targetSignature, null, null); + typedField.visitEnd(); + } + + private void createWriteMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { + + String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + String methodSignature = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "writeGenerated", methodDescriptor, methodSignature, + new String[] { FIELD_EXCEPTION_CLASS }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + String generatedClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ASTORE, 3); + mv.visitVarInsn(Opcodes.ILOAD, 1); + + // The last label is for the default switch + Label[] labels = new Label[fields.size()]; + Label errorLabel = new Label(); + Label returnLabel = new Label(); + + // Generate labels + for (int i = 0; i < fields.size(); i++) { + labels[i] = new Label(); + } + + mv.visitTableSwitchInsn(0, labels.length - 1, errorLabel, labels); + + for (int i = 0; i < fields.size(); i++) { + + Field field = fields.get(i); + Class outputType = field.getType(); + Class inputType = Primitives.wrap(outputType); + String typeDescriptor = Type.getDescriptor(outputType); + String inputPath = inputType.getName().replace('.', '/'); + + mv.visitLabel(labels[i]); + + // Push the compare object + if (i == 0) + mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); + else + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + // Only write to public non-final fields + if (isPublic(field) && isNonFinal(field)) { + mv.visitVarInsn(Opcodes.ALOAD, 3); + mv.visitVarInsn(Opcodes.ALOAD, 2); + + if (!outputType.isPrimitive()) + mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); + else + boxingHelper.unbox(Type.getType(outputType)); + + mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, field.getName(), typeDescriptor); + + } else { + // Use reflection. We don't have a choice, unfortunately. + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, generatedClassName, "writeReflected", "(ILjava/lang/Object;)V"); + } + + mv.visitJumpInsn(Opcodes.GOTO, returnLabel); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Invalid index "); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); + mv.visitInsn(Opcodes.ATHROW); + + mv.visitLabel(returnLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(5, 4); + mv.visitEnd(); + } + + private void createReadMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "readGenerated", "(I)Ljava/lang/Object;", null, + new String[] { "com/comphenix/protocol/reflect/FieldAccessException" }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + String generatedClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ASTORE, 2); + mv.visitVarInsn(Opcodes.ILOAD, 1); + + // The last label is for the default switch + Label[] labels = new Label[fields.size()]; + Label errorLabel = new Label(); + + // Generate labels + for (int i = 0; i < fields.size(); i++) { + labels[i] = new Label(); + } + + mv.visitTableSwitchInsn(0, fields.size() - 1, errorLabel, labels); + + for (int i = 0; i < fields.size(); i++) { + + Field field = fields.get(i); + Class outputType = field.getType(); + String typeDescriptor = Type.getDescriptor(outputType); + + mv.visitLabel(labels[i]); + + // Push the compare object + if (i == 0) + mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); + else + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + // Note that byte code cannot access non-public fields + if (isPublic(field)) { + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.GETFIELD, targetName, field.getName(), typeDescriptor); + + boxingHelper.box(Type.getType(outputType)); + } else { + // We have to use reflection for private and protected fields. + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, generatedClassName, "readReflected", "(I)Ljava/lang/Object;"); + } + + mv.visitInsn(Opcodes.ARETURN); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Invalid index "); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); + mv.visitInsn(Opcodes.ATHROW); + mv.visitMaxs(5, 3); + mv.visitEnd(); + } + + private void createConstructor(ClassWriter cw, String className, String targetSignature, String targetName) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", + "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", + "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", null); + String fullClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "", "()V"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, fullClassName, "initialize", "(L" + SUPER_CLASS + ";)V"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, SUPER_CLASS, "getTarget", "()Ljava/lang/Object;"); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "target", "Ljava/lang/Object;"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, fullClassName, "target", "Ljava/lang/Object;"); + mv.visitTypeInsn(Opcodes.CHECKCAST, targetName); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;"); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(2, 3); + mv.visitEnd(); + } +} From 93da193428eefa62fca64928521cb92f21a9f99d Mon Sep 17 00:00:00 2001 From: Kristian Date: Sat, 27 Apr 2013 01:03:20 +0200 Subject: [PATCH 34/46] Improve JavaDoc. --- .../protocol/error/DetailedErrorReporter.java | 69 +++++++++++++++++++ .../protocol/error/ErrorReporter.java | 9 ++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java index 80ba3d37..ce3574a8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java @@ -141,6 +141,13 @@ public class DetailedErrorReporter implements ErrorReporter { reportMinimalNoSpam(sender, methodName, error); } + /** + * Report a problem with a given method and plugin, ensuring that we don't exceed the maximum number of error reports. + * @param sender - the component that observed this exception. + * @param methodName - the method name. + * @param error - the error itself. + * @return TRUE if the error was printed, FALSE if it was suppressed. + */ public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) { String pluginName = PacketAdapter.getPluginName(sender); AtomicInteger counter = warningCount.get(pluginName); @@ -368,63 +375,125 @@ public class DetailedErrorReporter implements ErrorReporter { return test instanceof String || Primitives.isWrapperType(test.getClass()); } + /** + * Retrieve the current number of errors printed through {@link #reportDetailed(Object, Report)}. + * @return Number of errors printed. + */ public int getErrorCount() { return internalErrorCount.get(); } + /** + * Set the number of errors printed. + * @param errorCount - new number of errors printed. + */ public void setErrorCount(int errorCount) { internalErrorCount.set(errorCount); } + /** + * Retrieve the maximum number of errors we can print before we begin suppressing errors. + * @return Maximum number of errors. + */ public int getMaxErrorCount() { return maxErrorCount; } + /** + * Set the maximum number of errors we can print before we begin suppressing errors. + * @param maxErrorCount - new max count. + */ public void setMaxErrorCount(int maxErrorCount) { this.maxErrorCount = maxErrorCount; } /** * Adds the given global parameter. It will be included in every error report. + *

+ * Both key and value must be non-null. * @param key - name of parameter. * @param value - the global parameter itself. */ public void addGlobalParameter(String key, Object value) { + if (key == null) + throw new IllegalArgumentException("key cannot be NULL."); + if (value == null) + throw new IllegalArgumentException("value cannot be NULL."); + globalParameters.put(key, value); } + /** + * Retrieve a global parameter by its key. + * @param key - key of the parameter to retrieve. + * @return The value of the global parameter, or NULL if not found. + */ public Object getGlobalParameter(String key) { + if (key == null) + throw new IllegalArgumentException("key cannot be NULL."); + return globalParameters.get(key); } + /** + * Reset all global parameters. + */ public void clearGlobalParameters() { globalParameters.clear(); } + /** + * Retrieve a set of every registered global parameter. + * @return Set of all registered global parameters. + */ public Set globalParameters() { return globalParameters.keySet(); } + /** + * Retrieve the support URL that will be added to all detailed reports. + * @return Support URL. + */ public String getSupportURL() { return supportURL; } + /** + * Set the support URL that will be added to all detailed reports. + * @param supportURL - the new support URL. + */ public void setSupportURL(String supportURL) { this.supportURL = supportURL; } + /** + * Retrieve the prefix to apply to every line in the error reports. + * @return Error report prefix. + */ public String getPrefix() { return prefix; } + /** + * Set the prefix to apply to every line in the error reports. + * @param prefix - new prefix. + */ public void setPrefix(String prefix) { this.prefix = prefix; } + /** + * Retrieve the current logger that is used to print all reports. + * @return The current logger. + */ public Logger getLogger() { return logger; } + /** + * Set the current logger that is used to print all reports. + * @param logger - new logger. + */ public void setLogger(Logger logger) { this.logger = logger; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java index 7d45cdde..fde3334f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java @@ -21,9 +21,14 @@ import org.bukkit.plugin.Plugin; import com.comphenix.protocol.error.Report.ReportBuilder; +/** + * Represents an object that can forward an error {@link Report} to the display and permanent storage. + * + * @author Kristian + */ public interface ErrorReporter { /** - * Prints a small minimal error report about an exception from another plugin. + * Prints a small minimal error report regarding an exception from another plugin. * @param sender - the other plugin. * @param methodName - name of the caller method. * @param error - the exception itself. @@ -31,7 +36,7 @@ public interface ErrorReporter { public abstract void reportMinimal(Plugin sender, String methodName, Throwable error); /** - * Prints a small minimal error report about an exception from another plugin. + * Prints a small minimal error report regarding an exception from another plugin. * @param sender - the other plugin. * @param methodName - name of the caller method. * @param error - the exception itself. From cecf80250cf4edebdb088275d3e4a4973d8dd564 Mon Sep 17 00:00:00 2001 From: Kristian Date: Sat, 27 Apr 2013 02:24:49 +0200 Subject: [PATCH 35/46] Don't print pointless warnings on Spigot. --- .../comphenix/protocol/ProtocolLibrary.java | 9 +- .../protocol/error/BasicErrorReporter.java | 96 +++++++++++++++++++ .../error/DelegatedErrorReporter.java | 80 ++++++++++++++++ .../protocol/error/DetailedErrorReporter.java | 1 - .../injector/player/InjectedArrayList.java | 12 +-- .../injector/player/PlayerInjector.java | 2 +- .../injector/spigot/SpigotPacketInjector.java | 25 ++++- .../reflect/compiler/BackgroundCompiler.java | 17 ++-- 8 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 8fead494..6d4eb893 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -33,6 +33,7 @@ import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.async.AsyncFilterManager; +import com.comphenix.protocol.error.BasicErrorReporter; import com.comphenix.protocol.error.DetailedErrorReporter; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.Report; @@ -92,7 +93,7 @@ public class ProtocolLibrary extends JavaPlugin { private static PacketFilterManager protocolManager; // Error reporter - private static ErrorReporter reporter; + private static ErrorReporter reporter = new BasicErrorReporter(); // Metrics and statistisc private Statistics statistisc; @@ -490,7 +491,9 @@ public class ProtocolLibrary extends JavaPlugin { unhookTask.close(); protocolManager = null; statistisc = null; - reporter = null; + + // To clean up global parameters + reporter = new BasicErrorReporter(); // Leaky ClassLoader begone! if (updater == null || updater.getResult() != UpdateResult.SUCCESS) { @@ -515,6 +518,8 @@ public class ProtocolLibrary extends JavaPlugin { /** * Retrieve the current error reporter. + *

+ * This is guaranteed to not be NULL in 2.5.0 and later. * @return Current error reporter. */ public static ErrorReporter getErrorReporter() { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java new file mode 100644 index 00000000..69226d57 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java @@ -0,0 +1,96 @@ +package com.comphenix.protocol.error; + +import java.io.PrintStream; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.Report.ReportBuilder; +import com.comphenix.protocol.reflect.PrettyPrinter; + +/** + * Represents a basic error reporter that prints error reports to the standard error stream. + *

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

+ * To cancel a report, return NULL. + * @param sender - the sender component. + * @param report - the error report. + * @param detailed - whether or not the report will be displayed in detail. + * @return The report to pass on, or NULL to cancel it. + */ + protected Report filterReport(Object sender, Report report, boolean detailed) { + return report; + } + + @Override + public void reportWarning(Object sender, ReportBuilder reportBuilder) { + reportWarning(sender, reportBuilder.build()); + } + + @Override + public void reportDetailed(Object sender, ReportBuilder reportBuilder) { + reportDetailed(sender, reportBuilder.build()); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java index ce3574a8..6953d437 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java @@ -342,7 +342,6 @@ public class DetailedErrorReporter implements ErrorReporter { * @return String representation. */ protected String getStringDescription(Object value) { - // We can't only rely on toString. if (value == null) { return "[NULL]"; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java index d8534215..33e45f63 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Set; import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.Report; import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.injector.ListenerInvoker; @@ -88,15 +87,10 @@ class InjectedArrayList extends ArrayList { return true; } catch (InvocationTargetException e) { - ErrorReporter reporter = ProtocolLibrary.getErrorReporter(); - // Prefer to report this to the user, instead of risking sending it to Minecraft - if (reporter != null) { - reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet)); - } else { - System.out.println("[ProtocolLib] Reverting cancelled packet failed."); - e.printStackTrace(); - } + ProtocolLibrary.getErrorReporter().reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet) + ); // Failure return false; 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 f1f177da..02036a07 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java @@ -368,7 +368,7 @@ public abstract class PlayerInjector implements SocketInjector { return null; hasProxyType = true; - reporter.reportWarning(this, Report.newBuilder(REPORT_DETECTED_CUSTOM_SERVER_HANDLER).callerParam(notchEntity, serverField)); + reporter.reportWarning(this, Report.newBuilder(REPORT_DETECTED_CUSTOM_SERVER_HANDLER).callerParam(serverField)); // No? Is it a Proxy type? try { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java index 30d68021..71b3a1e3 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java @@ -22,7 +22,9 @@ import net.sf.cglib.proxy.NoOp; import com.comphenix.protocol.Packets; import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.DelegatedErrorReporter; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.ListenerInvoker; @@ -275,7 +277,8 @@ public class SpigotPacketInjector implements SpigotPacketListener { if (dummyInjector == null) { // Inject the network manager try { - NetworkObjectInjector created = new NetworkObjectInjector(classLoader, reporter, null, invoker, null); + NetworkObjectInjector created = new NetworkObjectInjector( + classLoader, filterImpossibleWarnings(reporter), null, invoker, null); if (MinecraftReflection.isLoginHandler(connection)) { created.initialize(connection); @@ -303,6 +306,23 @@ public class SpigotPacketInjector implements SpigotPacketListener { return dummyInjector; } + /** + * Return a delegated error reporter that ignores certain warnings that are irrelevant on Spigot. + * @param reporter - error reporter to delegate. + * @return The filtered error reporter. + */ + private ErrorReporter filterImpossibleWarnings(ErrorReporter reporter) { + return new DelegatedErrorReporter(reporter) { + @Override + protected Report filterReport(Object sender, Report report, boolean detailed) { + // This doesn't matter - ignore it + if (report.getType() == NetworkObjectInjector.REPORT_DETECTED_CUSTOM_SERVER_HANDLER) + return null; + return report; + } + }; + } + /** * Save a given player injector for later. * @param networkManager - the associated network manager. @@ -400,7 +420,8 @@ public class SpigotPacketInjector implements SpigotPacketListener { */ void injectPlayer(Player player) { try { - NetworkObjectInjector dummy = new NetworkObjectInjector(classLoader, reporter, player, invoker, null); + NetworkObjectInjector dummy = new NetworkObjectInjector( + classLoader, filterImpossibleWarnings(reporter), player, invoker, null); dummy.initializePlayer(player); // Save this player for the network manager diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java index 6653916d..19e9e56c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -29,8 +29,6 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; - import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.Report; import com.comphenix.protocol.error.ReportType; @@ -122,11 +120,13 @@ public class BackgroundCompiler { } // Avoid "Constructor call must be the first statement". - private void initializeCompiler(ClassLoader loader, @Nullable ErrorReporter reporter, ExecutorService executor) { + private void initializeCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) { if (loader == null) throw new IllegalArgumentException("loader cannot be NULL"); if (executor == null) throw new IllegalArgumentException("executor cannot be NULL"); + if (reporter == null) + throw new IllegalArgumentException("reporter cannot be NULL."); this.compiler = new StructureCompiler(loader); this.reporter = reporter; @@ -226,14 +226,9 @@ public class BackgroundCompiler { setEnabled(false); // Inform about this error as best as we can - if (reporter != null) { - reporter.reportDetailed(BackgroundCompiler.this, - Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e) - ); - } else { - System.err.println("Exception occured in structure compiler: "); - e.printStackTrace(); - } + reporter.reportDetailed(BackgroundCompiler.this, + Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e) + ); } // We'll also return the new structure modifier From bec05967d3c2889f382e592285d674e4ad6f1d32 Mon Sep 17 00:00:00 2001 From: Kristian Date: Sat, 27 Apr 2013 02:48:39 +0200 Subject: [PATCH 36/46] Mark field as volatile. --- .../protocol/injector/spigot/SpigotPacketInjector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 71b3a1e3..555a5304 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java @@ -49,7 +49,7 @@ public class SpigotPacketInjector implements SpigotPacketListener { private static volatile boolean classChecked; // Retrieve the entity player from a PlayerConnection - private static Field playerConnectionPlayer; + private static volatile Field playerConnectionPlayer; // Packets that are not to be processed by the filters private Set ignoredPackets = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); From 8964246e22b8ee5c3d1f10ee03d6c18b4f8438dc Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 28 Apr 2013 17:27:58 +0200 Subject: [PATCH 37/46] Makes more sense to put this in the reflect lookup. --- .../server/AbstractInputStreamLookup.java | 204 +++++----- .../server/InputStreamReflectLookup.java | 355 ++++++++++-------- 2 files changed, 277 insertions(+), 282 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java index 2ed52ae4..5fde4cdc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java @@ -1,119 +1,87 @@ -package com.comphenix.protocol.injector.server; - -import java.io.FilterInputStream; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.net.Socket; -import java.net.SocketAddress; -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; - -public abstract class AbstractInputStreamLookup { - // Used to access the inner input stream of a filtered input stream - private static Field filteredInputField; - - // Error reporter - protected final ErrorReporter reporter; - - // Reference to the server itself - protected final Server server; - - protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) { - this.reporter = reporter; - this.server = server; - } - - /** - * Retrieve the underlying input stream that is associated with a given filter input stream. - * @param filtered - the filter input stream. - * @return The underlying input stream that is being filtered. - * @throws FieldAccessException Unable to access input stream. - */ - protected static InputStream getInputStream(FilterInputStream filtered) { - if (filteredInputField == null) - filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). - getFieldByType("in", InputStream.class); - - InputStream current = filtered; - - try { - // Iterate until we find the real input stream - while (current instanceof FilterInputStream) { - current = (InputStream) FieldUtils.readField(filteredInputField, current, true); - } - return current; - } catch (IllegalAccessException e) { - throw new FieldAccessException("Cannot access filtered input field.", e); - } - } - - /** - * Inject the given server thread or dedicated connection. - * @param container - class that contains a ServerSocket field. - */ - public abstract void inject(Object container); - - /** - * Invoked when the world has loaded. - */ - public abstract void postWorldLoaded(); - - /** - * Retrieve the associated socket injector for a player. - * @param input - the indentifying filtered input stream. - * @return The socket injector we have associated with this player. - */ - public abstract SocketInjector waitSocketInjector(InputStream input); - - /** - * Retrieve an injector by its socket. - * @param socket - the socket. - * @return The socket injector. - */ - public abstract SocketInjector waitSocketInjector(Socket socket); - - /** - * Retrieve a injector by its address. - * @param address - the address of the socket. - * @return The socket injector, or NULL if not found. - */ - public abstract SocketInjector waitSocketInjector(SocketAddress address); - - /** - * Attempt to get a socket injector without blocking the thread. - * @param address - the address to lookup. - * @return The socket injector, or NULL if not found. - */ - public abstract SocketInjector peekSocketInjector(SocketAddress address); - - /** - * Associate a given socket address to the provided socket injector. - * @param address - the socket address to associate. - * @param injector - the injector. - */ - public abstract void setSocketInjector(SocketAddress address, SocketInjector injector); - - /** - * If a player can hold a reference to its parent injector, this method will update that reference. - * @param previous - the previous injector. - * @param current - the new injector. - */ - protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { - Player player = previous.getPlayer(); - - // Default implementation - if (player instanceof InjectorContainer) { - TemporaryPlayerFactory.setInjectorInPlayer(player, current); - } - } - - /** - * Invoked when the injection should be undone. - */ - public abstract void cleanupAll(); +package com.comphenix.protocol.injector.server; + +import java.io.InputStream; +import java.net.Socket; +import java.net.SocketAddress; +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.error.ErrorReporter; + +public abstract class AbstractInputStreamLookup { + // Error reporter + protected final ErrorReporter reporter; + + // Reference to the server itself + protected final Server server; + + protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) { + this.reporter = reporter; + this.server = server; + } + + /** + * Inject the given server thread or dedicated connection. + * @param container - class that contains a ServerSocket field. + */ + public abstract void inject(Object container); + + /** + * Invoked when the world has loaded. + */ + public abstract void postWorldLoaded(); + + /** + * Retrieve the associated socket injector for a player. + * @param input - the indentifying filtered input stream. + * @return The socket injector we have associated with this player. + */ + public abstract SocketInjector waitSocketInjector(InputStream input); + + /** + * Retrieve an injector by its socket. + * @param socket - the socket. + * @return The socket injector. + */ + public abstract SocketInjector waitSocketInjector(Socket socket); + + /** + * Retrieve a injector by its address. + * @param address - the address of the socket. + * @return The socket injector, or NULL if not found. + */ + public abstract SocketInjector waitSocketInjector(SocketAddress address); + + /** + * Attempt to get a socket injector without blocking the thread. + * @param address - the address to lookup. + * @return The socket injector, or NULL if not found. + */ + public abstract SocketInjector peekSocketInjector(SocketAddress address); + + /** + * Associate a given socket address to the provided socket injector. + * @param address - the socket address to associate. + * @param injector - the injector. + */ + public abstract void setSocketInjector(SocketAddress address, SocketInjector injector); + + /** + * If a player can hold a reference to its parent injector, this method will update that reference. + * @param previous - the previous injector. + * @param current - the new injector. + */ + protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { + Player player = previous.getPlayer(); + + // Default implementation + if (player instanceof InjectorContainer) { + TemporaryPlayerFactory.setInjectorInPlayer(player, current); + } + } + + /** + * Invoked when the injection should be undone. + */ + public abstract void cleanupAll(); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java index f374e9e9..37f5d6b6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java @@ -1,164 +1,191 @@ -package com.comphenix.protocol.injector.server; - -import java.io.FilterInputStream; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.net.Socket; -import java.net.SocketAddress; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; - -import org.bukkit.Server; - -import com.comphenix.protocol.concurrency.BlockingHashMap; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.google.common.collect.MapMaker; - -class InputStreamReflectLookup extends AbstractInputStreamLookup { - // The default lookup timeout - private static final long DEFAULT_TIMEOUT = 2000; // ms - - // Using weak keys and values ensures that we will not hold up garbage collection - protected BlockingHashMap addressLookup = new BlockingHashMap(); - protected ConcurrentMap inputLookup = new MapMaker().weakValues().makeMap(); - - // The timeout - private final long injectorTimeout; - - public InputStreamReflectLookup(ErrorReporter reporter, Server server) { - this(reporter, server, DEFAULT_TIMEOUT); - } - - /** - * Initialize a reflect lookup with a given default injector timeout. - *

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

+ * This timeout defines the maximum amount of time to wait until an injector has been discovered. + * @param reporter - the error reporter. + * @param server - the current Bukkit server. + * @param injectorTimeout - the injector timeout. + */ + public InputStreamReflectLookup(ErrorReporter reporter, Server server, long injectorTimeout) { + super(reporter, server); + this.injectorTimeout = injectorTimeout; + } + + @Override + public void inject(Object container) { + // Do nothing + } + + @Override + public void postWorldLoaded() { + // Nothing again + } + + @Override + public SocketInjector peekSocketInjector(SocketAddress address) { + try { + return addressLookup.get(address, 0, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Whatever + return null; + } + } + + @Override + public SocketInjector waitSocketInjector(SocketAddress address) { + try { + // Note that we actually SWALLOW interrupts here - this is because Minecraft uses interrupts to + // periodically wake up waiting readers and writers. We have to wait for the dedicated server thread + // to catch up, so we'll swallow these interrupts. + // + // TODO: Consider if we should raise the thread priority of the dedicated server listener thread. + return addressLookup.get(address, injectorTimeout, TimeUnit.MILLISECONDS, true); + } catch (InterruptedException e) { + // This cannot be! + throw new IllegalStateException("Impossible exception occured!", e); + } + } + + @Override + public SocketInjector waitSocketInjector(Socket socket) { + return waitSocketInjector(socket.getRemoteSocketAddress()); + } + + @Override + public SocketInjector waitSocketInjector(InputStream input) { + try { + SocketAddress address = waitSocketAddress(input); + + // Guard against NPE + if (address != null) + return waitSocketInjector(address); + else + return null; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot find or access socket field for " + input, e); + } + } + + /** + * Use reflection to get the underlying socket address from an input stream. + * @param stream - the socket stream to lookup. + * @return The underlying socket address, or NULL if not found. + * @throws IllegalAccessException Unable to access socket field. + */ + private SocketAddress waitSocketAddress(InputStream stream) throws IllegalAccessException { + // Extra check, just in case + if (stream instanceof FilterInputStream) + return waitSocketAddress(getInputStream((FilterInputStream) stream)); + + SocketAddress result = inputLookup.get(stream); + + if (result == null) { + Socket socket = lookupSocket(stream); + + // Save it + result = socket.getRemoteSocketAddress(); + inputLookup.put(stream, result); + } + return result; + } + + /** + * Retrieve the underlying input stream that is associated with a given filter input stream. + * @param filtered - the filter input stream. + * @return The underlying input stream that is being filtered. + * @throws FieldAccessException Unable to access input stream. + */ + protected static InputStream getInputStream(FilterInputStream filtered) { + if (filteredInputField == null) + filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). + getFieldByType("in", InputStream.class); + + InputStream current = filtered; + + try { + // Iterate until we find the real input stream + while (current instanceof FilterInputStream) { + current = (InputStream) FieldUtils.readField(filteredInputField, current, true); + } + return current; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access filtered input field.", e); + } + } + + @Override + public void setSocketInjector(SocketAddress address, SocketInjector injector) { + if (address == null) + throw new IllegalArgumentException("address cannot be NULL"); + if (injector == null) + throw new IllegalArgumentException("injector cannot be NULL."); + + SocketInjector previous = addressLookup.put(address, injector); + + // Any previous temporary players will also be associated + if (previous != null) { + // Update the reference to any previous injector + onPreviousSocketOverwritten(previous, injector); + } + } + + @Override + public void cleanupAll() { + // Do nothing + } + + /** + * Lookup the underlying socket of a stream through reflection. + * @param stream - the socket stream. + * @return The underlying socket. + * @throws IllegalAccessException If reflection failed. + */ + private static Socket lookupSocket(InputStream stream) throws IllegalAccessException { + if (stream instanceof FilterInputStream) { + return lookupSocket(getInputStream((FilterInputStream) stream)); + } else { + // Just do it + Field socketField = FuzzyReflection.fromObject(stream, true). + getFieldByType("socket", Socket.class); + + return (Socket) FieldUtils.readField(socketField, stream, true); + } + } +} From 8970cd915ae3e9cc8354b431a5b2ff1d8502eed7 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 29 Apr 2013 16:35:02 +0200 Subject: [PATCH 38/46] Move the last update time stamp to a separate file. FIXES TICKET 86. --- .../comphenix/protocol/ProtocolConfig.java | 121 +++++++++++++++--- 1 file changed, 101 insertions(+), 20 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index d6571e85..269bcb04 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -18,12 +18,15 @@ package com.comphenix.protocol; import java.io.File; +import java.io.IOException; import org.bukkit.configuration.Configuration; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.google.common.base.Charsets; +import com.google.common.io.Files; /** * Represents the configuration of ProtocolLib. @@ -31,6 +34,7 @@ import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; * @author Kristian */ class ProtocolConfig { + private static final String LAST_UPDATE_FILE = "lastupdate"; private static final String SECTION_GLOBAL = "global"; private static final String SECTION_AUTOUPDATER = "auto updater"; @@ -48,7 +52,6 @@ class ProtocolConfig { private static final String UPDATER_NOTIFY = "notify"; private static final String UPDATER_DOWNLAD = "download"; private static final String UPDATER_DELAY = "delay"; - private static final String UPDATER_LAST_TIME = "last"; // Defaults private static final long DEFAULT_UPDATER_DELAY = 43200; @@ -60,6 +63,11 @@ class ProtocolConfig { private ConfigurationSection global; private ConfigurationSection updater; + // Last update time + private long lastUpdateTime; + private boolean configChanged; + private boolean valuesChanged; + public ProtocolConfig(Plugin plugin) { this(plugin, plugin.getConfig()); } @@ -73,10 +81,64 @@ class ProtocolConfig { * Reload configuration file. */ public void reloadConfig() { + // Reset + configChanged = false; + valuesChanged = false; + this.config = plugin.getConfig(); + this.lastUpdateTime = loadLastUpdate(); loadSections(!loadingSections); } + /** + * Load the last update time stamp from the file system. + * @return Last update time stamp. + */ + private long loadLastUpdate() { + File dataFile = getLastUpdateFile(); + + if (dataFile.exists()) { + try { + return Long.parseLong(Files.toString(dataFile, Charsets.UTF_8)); + } catch (NumberFormatException e) { + throw new RuntimeException("Cannot parse " + dataFile + " as a number.", e); + } catch (IOException e) { + throw new RuntimeException("Cannot read " + dataFile, e); + } + } else { + // Default last update + return 0; + } + } + + /** + * Store the given time stamp. + * @param value - time stamp to store. + */ + private void saveLastUpdate(long value) { + File dataFile = getLastUpdateFile(); + + // The data folder must exist + dataFile.getParentFile().mkdirs(); + + if (dataFile.exists()) + dataFile.delete(); + + try { + Files.write(Long.toString(value), dataFile, Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Cannot write " + dataFile, e); + } + } + + /** + * Retrieve the file that is used to store the update time stamp. + * @return File storing the update time stamp. + */ + private File getLastUpdateFile() { + return new File(plugin.getDataFolder(), LAST_UPDATE_FILE); + } + /** * Load data sections. * @param copyDefaults - whether or not to copy configuration defaults. @@ -103,6 +165,17 @@ class ProtocolConfig { System.out.println("[ProtocolLib] Created default configuration."); } } + + /** + * Set a particular configuration key value pair. + * @param section - the configuration root. + * @param path - the path to the key. + * @param value - the value to set. + */ + private void setConfig(ConfigurationSection section, String path, Object value) { + configChanged = true; + section.set(path, value); + } /** * Retrieve a reference to the configuration file. @@ -125,7 +198,7 @@ class ProtocolConfig { * @param value - TRUE to do this automatically, FALSE otherwise. */ public void setAutoNotify(boolean value) { - updater.set(UPDATER_NOTIFY, value); + setConfig(updater, UPDATER_NOTIFY, value); } /** @@ -141,7 +214,7 @@ class ProtocolConfig { * @param value - TRUE if it should. FALSE otherwise. */ public void setAutoDownload(boolean value) { - updater.set(UPDATER_DOWNLAD, value); + setConfig(updater, UPDATER_DOWNLAD, value); } /** @@ -159,7 +232,7 @@ class ProtocolConfig { * @param value - TRUE if it is enabled, FALSE otherwise. */ public void setDebug(boolean value) { - global.set(DEBUG_MODE_ENABLED, value); + setConfig(global, DEBUG_MODE_ENABLED, value); } /** @@ -181,17 +254,9 @@ class ProtocolConfig { // Silently fix the delay if (delaySeconds < DEFAULT_UPDATER_DELAY) delaySeconds = DEFAULT_UPDATER_DELAY; - updater.set(UPDATER_DELAY, delaySeconds); + setConfig(updater, UPDATER_DELAY, delaySeconds); } - /** - * Retrieve the last time we updated, in seconds since 1970.01.01 00:00. - * @return Last update time. - */ - public long getAutoLastTime() { - return updater.getLong(UPDATER_LAST_TIME, 0); - } - /** * The version of Minecraft to ignore the built-in safety feature. * @return The version to ignore ProtocolLib's satefy. @@ -209,7 +274,7 @@ class ProtocolConfig { * @param ignoreVersion - the version of Minecraft where the satefy will be disabled. */ public void setIgnoreVersionCheck(String ignoreVersion) { - global.set(IGNORE_VERSION_CHECK, ignoreVersion); + setConfig(global, IGNORE_VERSION_CHECK, ignoreVersion); } /** @@ -228,7 +293,7 @@ class ProtocolConfig { * @param enabled - whether or not metrics is enabled. */ public void setMetricsEnabled(boolean enabled) { - global.set(METRICS_ENABLED, enabled); + setConfig(global, METRICS_ENABLED, enabled); } /** @@ -247,7 +312,15 @@ class ProtocolConfig { * @param enabled - TRUE if is enabled/running, FALSE otherwise. */ public void setBackgroundCompilerEnabled(boolean enabled) { - global.set(BACKGROUND_COMPILER_ENABLED, enabled); + setConfig(global, BACKGROUND_COMPILER_ENABLED, enabled); + } + + /** + * Retrieve the last time we updated, in seconds since 1970.01.01 00:00. + * @return Last update time. + */ + public long getAutoLastTime() { + return lastUpdateTime; } /** @@ -255,7 +328,8 @@ class ProtocolConfig { * @param lastTimeSeconds - new last update time. */ public void setAutoLastTime(long lastTimeSeconds) { - updater.set(UPDATER_LAST_TIME, lastTimeSeconds); + this.valuesChanged = true; + this.lastUpdateTime = lastTimeSeconds; } /** @@ -273,7 +347,7 @@ class ProtocolConfig { * @param name - name of the script engine to use. */ public void setScriptEngineName(String name) { - global.set(SCRIPT_ENGINE_NAME, name); + setConfig(global, SCRIPT_ENGINE_NAME, name); } /** @@ -305,13 +379,20 @@ class ProtocolConfig { * @return Injection method. */ public void setInjectionMethod(PlayerInjectHooks hook) { - global.set(INJECTION_METHOD, hook.name()); + setConfig(global, INJECTION_METHOD, hook.name()); } /** * Save the current configuration file. */ public void saveAll() { - plugin.saveConfig(); + if (valuesChanged) + saveLastUpdate(lastUpdateTime); + if (configChanged) + plugin.saveConfig(); + + // And we're done + valuesChanged = false; + configChanged = false; } } From c8138117217a8c5fc633c94b48d02d5c0d871cce Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 30 Apr 2013 03:17:59 +0200 Subject: [PATCH 39/46] No point verifying the load order for ProtocolLib. Also removed a debug message. --- .../com/comphenix/protocol/CommandPacket.java | 1 - .../protocol/injector/PluginVerifier.java | 15 +++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index a12b5c50..72eac4c4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -348,7 +348,6 @@ class CommandPacket extends CommandBase { else if (side.isForServer()) supported.addAll(Packets.Server.getSupported()); - System.out.println("Supported for " + side + ": " + supported); return supported; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java index 9be75b1b..ebb98a26 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java @@ -120,12 +120,15 @@ class PluginVerifier { if (plugin == null) throw new IllegalArgumentException("plugin cannot be NULL."); - if (!loadedAfter.contains(plugin.getName())) { - if (verifyLoadOrder(dependency, plugin)) { - // Memorize - loadedAfter.add(plugin.getName()); - } else { - return VerificationResult.NO_DEPEND; + // Skip the load order check for ProtocolLib itself + if (!dependency.equals(plugin)) { + if (!loadedAfter.contains(plugin.getName())) { + if (verifyLoadOrder(dependency, plugin)) { + // Memorize + loadedAfter.add(plugin.getName()); + } else { + return VerificationResult.NO_DEPEND; + } } } From 0fc639697434546610a31e413175e7e1bab3375b Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 5 May 2013 23:55:02 +0200 Subject: [PATCH 40/46] ProtocolLib seems to work fine for 1.5.2 too. --- .../src/main/java/com/comphenix/protocol/ProtocolLibrary.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 6d4eb893..b93c662a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -80,7 +80,7 @@ public class ProtocolLibrary extends JavaPlugin { /** * The maximum version ProtocolLib has been tested with, */ - private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.1"; + private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.2"; /** * The number of milliseconds per second. From 8d814d2d9ced9c84ab6ca81dacd965d86ecb8ffe Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 6 May 2013 18:37:08 +0200 Subject: [PATCH 41/46] Implement "isOnline" for temporary players. This corrects the issue seen on http://pastebin.com/C4D8jsja --- .../server/TemporaryPlayerFactory.java | 392 +++++++++--------- 1 file changed, 197 insertions(+), 195 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java index 91d1fed6..8c2e0e05 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java @@ -1,195 +1,197 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.server; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.CallbackFilter; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; -import net.sf.cglib.proxy.NoOp; - -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.injector.PacketConstructor; -import com.comphenix.protocol.reflect.FieldAccessException; - -/** - * Create fake player instances that represents pre-authenticated clients. - */ -public class TemporaryPlayerFactory { - // Helpful constructors - private final PacketConstructor chatPacket; - - // Prevent too many class creations - private static CallbackFilter callbackFilter; - - public TemporaryPlayerFactory() { - chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" }); - } - - /** - * Retrieve the injector from a given player if it contains one. - * @param player - the player that may contain a reference to a player injector. - * @return The referenced player injector, or NULL if none can be found. - */ - public static SocketInjector getInjectorFromPlayer(Player player) { - if (player instanceof InjectorContainer) { - return ((InjectorContainer) player).getInjector(); - } - return null; - } - - /** - * Set the player injector, if possible. - * @param player - the player to update. - * @param injector - the injector to store. - */ - public static void setInjectorInPlayer(Player player, SocketInjector injector) { - ((InjectorContainer) player).setInjector(injector); - } - - /** - * Construct a temporary player that supports a subset of every player command. - *

- * Supported methods include: - *

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

- * Note that a temporary player has not yet been assigned a name, and thus cannot be - * uniquely identified. Use the address instead. - * @param injector - the player injector used. - * @param server - the current server. - * @return A temporary player instance. - */ - public Player createTemporaryPlayer(final Server server) { - - // Default implementation - Callback implementation = new MethodInterceptor() { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - - String methodName = method.getName(); - SocketInjector injector = ((InjectorContainer) obj).getInjector(); - - if (injector == null) - throw new IllegalStateException("Unable to find injector."); - - // Use the socket to get the address - if (methodName.equalsIgnoreCase("getName")) - return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]"; - if (methodName.equalsIgnoreCase("getPlayer")) - return injector.getUpdatedPlayer(); - if (methodName.equalsIgnoreCase("getAddress")) - return injector.getAddress(); - if (methodName.equalsIgnoreCase("getServer")) - return server; - - try { - // Handle send message methods - if (methodName.equalsIgnoreCase("chat") || methodName.equalsIgnoreCase("sendMessage")) { - Object argument = args[0]; - - // Dynamic overloading - if (argument instanceof String) { - return sendMessage(injector, (String) argument); - } else if (argument instanceof String[]) { - for (String message : (String[]) argument) { - sendMessage(injector, message); - } - return null; - } - } - } catch (InvocationTargetException e) { - throw e.getCause(); - } - - // Also, handle kicking - if (methodName.equalsIgnoreCase("kickPlayer")) { - injector.disconnect((String) args[0]); - return null; - } - - // Ignore all other methods - throw new UnsupportedOperationException( - "The method " + method.getName() + " is not supported for temporary players."); - } - }; - - // Shared callback filter - if (callbackFilter == null) { - callbackFilter = new CallbackFilter() { - @Override - public int accept(Method method) { - // Do not override the object method or the superclass methods - if (method.getDeclaringClass().equals(Object.class) || - method.getDeclaringClass().equals(InjectorContainer.class)) - return 0; - else - return 1; - } - }; - } - - // CGLib is amazing - Enhancer ex = new Enhancer(); - ex.setSuperclass(InjectorContainer.class); - ex.setInterfaces(new Class[] { Player.class }); - ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation }); - ex.setCallbackFilter(callbackFilter); - - return (Player) ex.create(); - } - - /** - * Construct a temporary player with the given associated socket injector. - * @param server - the parent server. - * @param injector - the referenced socket injector. - * @return The temporary player. - */ - public Player createTemporaryPlayer(Server server, SocketInjector injector) { - Player temporary = createTemporaryPlayer(server); - - ((InjectorContainer) temporary).setInjector(injector); - return temporary; - } - - /** - * Send a message to the given client. - * @param injector - the injector representing the client. - * @param message - a message. - * @return Always NULL. - * @throws InvocationTargetException If the message couldn't be sent. - * @throws FieldAccessException If we were unable to construct the message packet. - */ - private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException { - injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false); - return null; - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.server; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; +import net.sf.cglib.proxy.NoOp; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.injector.PacketConstructor; +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Create fake player instances that represents pre-authenticated clients. + */ +public class TemporaryPlayerFactory { + // Helpful constructors + private final PacketConstructor chatPacket; + + // Prevent too many class creations + private static CallbackFilter callbackFilter; + + public TemporaryPlayerFactory() { + chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" }); + } + + /** + * Retrieve the injector from a given player if it contains one. + * @param player - the player that may contain a reference to a player injector. + * @return The referenced player injector, or NULL if none can be found. + */ + public static SocketInjector getInjectorFromPlayer(Player player) { + if (player instanceof InjectorContainer) { + return ((InjectorContainer) player).getInjector(); + } + return null; + } + + /** + * Set the player injector, if possible. + * @param player - the player to update. + * @param injector - the injector to store. + */ + public static void setInjectorInPlayer(Player player, SocketInjector injector) { + ((InjectorContainer) player).setInjector(injector); + } + + /** + * Construct a temporary player that supports a subset of every player command. + *

+ * Supported methods include: + *

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

+ * Note that a temporary player has not yet been assigned a name, and thus cannot be + * uniquely identified. Use the address instead. + * @param injector - the player injector used. + * @param server - the current server. + * @return A temporary player instance. + */ + public Player createTemporaryPlayer(final Server server) { + + // Default implementation + Callback implementation = new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + + String methodName = method.getName(); + SocketInjector injector = ((InjectorContainer) obj).getInjector(); + + if (injector == null) + throw new IllegalStateException("Unable to find injector."); + + // Use the socket to get the address + if (methodName.equalsIgnoreCase("isOnline")) + return injector.getSocket() != null && injector.getSocket().isConnected(); + if (methodName.equalsIgnoreCase("getName")) + return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]"; + if (methodName.equalsIgnoreCase("getPlayer")) + return injector.getUpdatedPlayer(); + if (methodName.equalsIgnoreCase("getAddress")) + return injector.getAddress(); + if (methodName.equalsIgnoreCase("getServer")) + return server; + + try { + // Handle send message methods + if (methodName.equalsIgnoreCase("chat") || methodName.equalsIgnoreCase("sendMessage")) { + Object argument = args[0]; + + // Dynamic overloading + if (argument instanceof String) { + return sendMessage(injector, (String) argument); + } else if (argument instanceof String[]) { + for (String message : (String[]) argument) { + sendMessage(injector, message); + } + return null; + } + } + } catch (InvocationTargetException e) { + throw e.getCause(); + } + + // Also, handle kicking + if (methodName.equalsIgnoreCase("kickPlayer")) { + injector.disconnect((String) args[0]); + return null; + } + + // Ignore all other methods + throw new UnsupportedOperationException( + "The method " + method.getName() + " is not supported for temporary players."); + } + }; + + // Shared callback filter + if (callbackFilter == null) { + callbackFilter = new CallbackFilter() { + @Override + public int accept(Method method) { + // Do not override the object method or the superclass methods + if (method.getDeclaringClass().equals(Object.class) || + method.getDeclaringClass().equals(InjectorContainer.class)) + return 0; + else + return 1; + } + }; + } + + // CGLib is amazing + Enhancer ex = new Enhancer(); + ex.setSuperclass(InjectorContainer.class); + ex.setInterfaces(new Class[] { Player.class }); + ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation }); + ex.setCallbackFilter(callbackFilter); + + return (Player) ex.create(); + } + + /** + * Construct a temporary player with the given associated socket injector. + * @param server - the parent server. + * @param injector - the referenced socket injector. + * @return The temporary player. + */ + public Player createTemporaryPlayer(Server server, SocketInjector injector) { + Player temporary = createTemporaryPlayer(server); + + ((InjectorContainer) temporary).setInjector(injector); + return temporary; + } + + /** + * Send a message to the given client. + * @param injector - the injector representing the client. + * @param message - a message. + * @return Always NULL. + * @throws InvocationTargetException If the message couldn't be sent. + * @throws FieldAccessException If we were unable to construct the message packet. + */ + private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException { + injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false); + return null; + } +} From a2b04e055a744af841fbd37a18399748f178e4c8 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 6 May 2013 18:56:19 +0200 Subject: [PATCH 42/46] Make it possible to cancel packets for asynchronous processing. --- .../protocol/async/PacketSendingQueue.java | 609 +++++++++--------- .../injector/PacketFilterManager.java | 1 + 2 files changed, 306 insertions(+), 304 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java index b9d5b7b0..2da2d220 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java @@ -1,304 +1,305 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.async; - -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.PriorityBlockingQueue; - -import org.bukkit.entity.Player; - -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.injector.PlayerLoggedOutException; -import com.comphenix.protocol.reflect.FieldAccessException; - -/** - * Represents packets ready to be transmitted to a client. - * @author Kristian - */ -abstract class PacketSendingQueue { - - public static final int INITIAL_CAPACITY = 10; - - private PriorityBlockingQueue sendingQueue; - - // Asynchronous packet sending - private Executor asynchronousSender; - - // Whether or not packet transmission must occur on a specific thread - private final boolean notThreadSafe; - - // Whether or not we've run the cleanup procedure - private boolean cleanedUp = false; - - /** - * Create a packet sending queue. - * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. - */ - public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { - this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY); - this.notThreadSafe = notThreadSafe; - this.asynchronousSender = asynchronousSender; - } - - /** - * Number of packet events in the queue. - * @return The number of packet events in the queue. - */ - public int size() { - return sendingQueue.size(); - } - - /** - * Enqueue a packet for sending. - * @param packet - packet to queue. - */ - public void enqueue(PacketEvent packet) { - sendingQueue.add(new PacketEventHolder(packet)); - } - - /** - * Invoked when one of the packets have finished processing. - * @param packetUpdated - the packet that has now been updated. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { - - AsyncMarker marker = packetUpdated.getAsyncMarker(); - - // Should we reorder the event? - if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { - PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); - - // "Cancel" the original event - packetUpdated.setCancelled(true); - - // Enqueue the copy with the new sending index - enqueue(copy); - } - - // Mark this packet as finished - marker.setProcessed(true); - trySendPackets(onMainThread); - } - - /*** - * Invoked when a list of packet IDs are no longer associated with any listeners. - * @param packetsRemoved - packets that no longer have any listeners. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { - - Set lookup = new HashSet(packetsRemoved); - - // Note that this is O(n), so it might be expensive - for (PacketEventHolder holder : sendingQueue) { - PacketEvent event = holder.getEvent(); - - if (lookup.contains(event.getPacketID())) { - event.getAsyncMarker().setProcessed(true); - } - } - - // This is likely to have changed the situation a bit - trySendPackets(onMainThread); - } - - /** - * Attempt to send any remaining packets. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public void trySendPackets(boolean onMainThread) { - // Whether or not to continue sending packets - boolean sending = true; - - // Transmit as many packets as we can - while (sending) { - PacketEventHolder holder = sendingQueue.poll(); - - if (holder != null) { - sending = processPacketHolder(onMainThread, holder); - - if (!sending) { - // Add it back again - sendingQueue.add(holder); - } - - } else { - // No more packets to send - sending = false; - } - } - } - - /** - * Invoked when a packet might be ready for transmission. - * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. - * @param holder - packet container. - * @return TRUE to continue sending packets, FALSE otherwise. - */ - private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { - PacketEvent current = holder.getEvent(); - AsyncMarker marker = current.getAsyncMarker(); - boolean hasExpired = marker.hasExpired(); - - // Guard in cause the queue is closed - if (cleanedUp) { - return true; - } - - // End condition? - if (marker.isProcessed() || hasExpired) { - if (hasExpired) { - // Notify timeout listeners - onPacketTimeout(current); - - // Recompute - marker = current.getAsyncMarker(); - hasExpired = marker.hasExpired(); - - // Could happen due to the timeout listeners - if (!marker.isProcessed() && !hasExpired) { - return false; - } - } - - // Is it okay to send the packet? - if (!current.isCancelled() && !hasExpired) { - // Make sure we're on the main thread - if (notThreadSafe) { - try { - boolean wantAsync = marker.isMinecraftAsync(current); - boolean wantSync = !wantAsync; - - // Wait for the next main thread heartbeat if we haven't fulfilled our promise - if (!onMainThread && wantSync) { - return false; - } - - // Let's give it what it wants - if (onMainThread && wantAsync) { - asynchronousSender.execute(new Runnable() { - @Override - public void run() { - // We know this isn't on the main thread - processPacketHolder(false, holder); - } - }); - - // Scheduler will do the rest - return true; - } - - } catch (FieldAccessException e) { - e.printStackTrace(); - - // Just drop the packet - return true; - } - } - - // Silently skip players that have logged out - if (isOnline(current.getPlayer())) { - sendPacket(current); - } - } - - // Drop the packet - return true; - } - - // Add it back and stop sending - return false; - } - - /** - * Invoked when a packet has timed out. - * @param event - the timed out packet. - */ - protected abstract void onPacketTimeout(PacketEvent event); - - private boolean isOnline(Player player) { - return player != null && player.isOnline(); - } - - /** - * Send every packet, regardless of the processing state. - */ - private void forceSend() { - while (true) { - PacketEventHolder holder = sendingQueue.poll(); - - if (holder != null) { - sendPacket(holder.getEvent()); - } else { - break; - } - } - } - - /** - * Whether or not the packet transmission must synchronize with the main thread. - * @return TRUE if it must, FALSE otherwise. - */ - public boolean isSynchronizeMain() { - return notThreadSafe; - } - - /** - * Transmit a packet, if it hasn't already. - * @param event - the packet to transmit. - */ - private void sendPacket(PacketEvent event) { - - AsyncMarker marker = event.getAsyncMarker(); - - try { - // Don't send a packet twice - if (marker != null && !marker.isTransmitted()) { - marker.sendPacket(event); - } - - } catch (PlayerLoggedOutException e) { - System.out.println(String.format( - "[ProtocolLib] Warning: Dropped packet index %s of ID %s", - marker.getOriginalSendingIndex(), event.getPacketID() - )); - - } catch (IOException e) { - // Just print the error - e.printStackTrace(); - } - } - - /** - * Automatically transmits every delayed packet. - */ - public void cleanupAll() { - if (!cleanedUp) { - // Note that the cleanup itself will always occur on the main thread - forceSend(); - - // And we're done - cleanedUp = true; - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.async; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.PriorityBlockingQueue; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Represents packets ready to be transmitted to a client. + * @author Kristian + */ +abstract class PacketSendingQueue { + + public static final int INITIAL_CAPACITY = 10; + + private PriorityBlockingQueue sendingQueue; + + // Asynchronous packet sending + private Executor asynchronousSender; + + // Whether or not packet transmission must occur on a specific thread + private final boolean notThreadSafe; + + // Whether or not we've run the cleanup procedure + private boolean cleanedUp = false; + + /** + * Create a packet sending queue. + * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. + */ + public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { + this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY); + this.notThreadSafe = notThreadSafe; + this.asynchronousSender = asynchronousSender; + } + + /** + * Number of packet events in the queue. + * @return The number of packet events in the queue. + */ + public int size() { + return sendingQueue.size(); + } + + /** + * Enqueue a packet for sending. + * @param packet - packet to queue. + */ + public void enqueue(PacketEvent packet) { + sendingQueue.add(new PacketEventHolder(packet)); + } + + /** + * Invoked when one of the packets have finished processing. + * @param packetUpdated - the packet that has now been updated. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { + + AsyncMarker marker = packetUpdated.getAsyncMarker(); + + // Should we reorder the event? + if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { + PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); + + // "Cancel" the original event + packetUpdated.setReadOnly(false); + packetUpdated.setCancelled(true); + + // Enqueue the copy with the new sending index + enqueue(copy); + } + + // Mark this packet as finished + marker.setProcessed(true); + trySendPackets(onMainThread); + } + + /*** + * Invoked when a list of packet IDs are no longer associated with any listeners. + * @param packetsRemoved - packets that no longer have any listeners. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { + + Set lookup = new HashSet(packetsRemoved); + + // Note that this is O(n), so it might be expensive + for (PacketEventHolder holder : sendingQueue) { + PacketEvent event = holder.getEvent(); + + if (lookup.contains(event.getPacketID())) { + event.getAsyncMarker().setProcessed(true); + } + } + + // This is likely to have changed the situation a bit + trySendPackets(onMainThread); + } + + /** + * Attempt to send any remaining packets. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public void trySendPackets(boolean onMainThread) { + // Whether or not to continue sending packets + boolean sending = true; + + // Transmit as many packets as we can + while (sending) { + PacketEventHolder holder = sendingQueue.poll(); + + if (holder != null) { + sending = processPacketHolder(onMainThread, holder); + + if (!sending) { + // Add it back again + sendingQueue.add(holder); + } + + } else { + // No more packets to send + sending = false; + } + } + } + + /** + * Invoked when a packet might be ready for transmission. + * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. + * @param holder - packet container. + * @return TRUE to continue sending packets, FALSE otherwise. + */ + private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { + PacketEvent current = holder.getEvent(); + AsyncMarker marker = current.getAsyncMarker(); + boolean hasExpired = marker.hasExpired(); + + // Guard in cause the queue is closed + if (cleanedUp) { + return true; + } + + // End condition? + if (marker.isProcessed() || hasExpired) { + if (hasExpired) { + // Notify timeout listeners + onPacketTimeout(current); + + // Recompute + marker = current.getAsyncMarker(); + hasExpired = marker.hasExpired(); + + // Could happen due to the timeout listeners + if (!marker.isProcessed() && !hasExpired) { + return false; + } + } + + // Is it okay to send the packet? + if (!current.isCancelled() && !hasExpired) { + // Make sure we're on the main thread + if (notThreadSafe) { + try { + boolean wantAsync = marker.isMinecraftAsync(current); + boolean wantSync = !wantAsync; + + // Wait for the next main thread heartbeat if we haven't fulfilled our promise + if (!onMainThread && wantSync) { + return false; + } + + // Let's give it what it wants + if (onMainThread && wantAsync) { + asynchronousSender.execute(new Runnable() { + @Override + public void run() { + // We know this isn't on the main thread + processPacketHolder(false, holder); + } + }); + + // Scheduler will do the rest + return true; + } + + } catch (FieldAccessException e) { + e.printStackTrace(); + + // Just drop the packet + return true; + } + } + + // Silently skip players that have logged out + if (isOnline(current.getPlayer())) { + sendPacket(current); + } + } + + // Drop the packet + return true; + } + + // Add it back and stop sending + return false; + } + + /** + * Invoked when a packet has timed out. + * @param event - the timed out packet. + */ + protected abstract void onPacketTimeout(PacketEvent event); + + private boolean isOnline(Player player) { + return player != null && player.isOnline(); + } + + /** + * Send every packet, regardless of the processing state. + */ + private void forceSend() { + while (true) { + PacketEventHolder holder = sendingQueue.poll(); + + if (holder != null) { + sendPacket(holder.getEvent()); + } else { + break; + } + } + } + + /** + * Whether or not the packet transmission must synchronize with the main thread. + * @return TRUE if it must, FALSE otherwise. + */ + public boolean isSynchronizeMain() { + return notThreadSafe; + } + + /** + * Transmit a packet, if it hasn't already. + * @param event - the packet to transmit. + */ + private void sendPacket(PacketEvent event) { + + AsyncMarker marker = event.getAsyncMarker(); + + try { + // Don't send a packet twice + if (marker != null && !marker.isTransmitted()) { + marker.sendPacket(event); + } + + } catch (PlayerLoggedOutException e) { + System.out.println(String.format( + "[ProtocolLib] Warning: Dropped packet index %s of ID %s", + marker.getOriginalSendingIndex(), event.getPacketID() + )); + + } catch (IOException e) { + // Just print the error + e.printStackTrace(); + } + } + + /** + * Automatically transmits every delayed packet. + */ + public void cleanupAll() { + if (!cleanedUp) { + // Note that the cleanup itself will always occur on the main thread + forceSend(); + + // And we're done + cleanedUp = true; + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 88b00209..5666765b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -496,6 +496,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok asyncFilterManager.enqueueSyncPacket(event, event.getAsyncMarker()); // The above makes a copy of the event, so it's safe to cancel it + event.setReadOnly(false); event.setCancelled(true); } } From 6af440789cf2ee1951ef0f3bd00527612c015b8f Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 7 May 2013 23:11:41 +0200 Subject: [PATCH 43/46] Invert the actual packet instance instead of a new instance. In NetworkFieldInjector, we use a "inverted" packet class to undo the data received counter when a packet has been cancelled. Previously, we would generate a proxy class that inherits from the class of the packet (Packet3Chat, etc.) along with its size() method (which is called and added to the counter, cancelling the other packet), but this doesn't work for packets that are dynamically sized such as Packet255KickDisconnect. Instead, we now pass the actual instance to the proxy class through a weak hash map. --- .../protocol/injector/ListenerInvoker.java | 133 +++++++++--------- .../injector/player/InjectedArrayList.java | 38 +++-- 2 files changed, 97 insertions(+), 74 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java index f27b64e5..52b702fc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java @@ -1,67 +1,68 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector; - -import com.comphenix.protocol.events.PacketEvent; - -/** - * Represents an object that initiate the packet listeners. - * - * @author Kristian - */ -public interface ListenerInvoker { - - /** - * Invokes the given packet event for every registered listener. - * @param event - the packet event to invoke. - */ - public abstract void invokePacketRecieving(PacketEvent event); - - /** - * Invokes the given packet event for every registered listener. - * @param event - the packet event to invoke. - */ - public abstract void invokePacketSending(PacketEvent event); - - /** - * Retrieve the associated ID of a packet. - * @param packet - the packet. - * @return The packet ID. - */ - public abstract int getPacketID(Object packet); - - /** - * Associate a given class with the given packet ID. Internal method. - * @param clazz - class to associate. - */ - public abstract void unregisterPacketClass(Class clazz); - - /** - * Remove a given class from the packet registry. Internal method. - * @param clazz - class to remove. - */ - public abstract void registerPacketClass(Class clazz, int packetID); - - /** - * Retrieves the correct packet class from a given packet ID. - * @param packetID - the packet ID. - * @param forceVanilla - whether or not to look for vanilla classes, not injected classes. - * @return The associated class. - */ - public abstract Class getPacketClassFromID(int packetID, boolean forceVanilla); +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector; + +import com.comphenix.protocol.events.PacketEvent; + +/** + * Represents an object that initiate the packet listeners. + * + * @author Kristian + */ +public interface ListenerInvoker { + + /** + * Invokes the given packet event for every registered listener. + * @param event - the packet event to invoke. + */ + public abstract void invokePacketRecieving(PacketEvent event); + + /** + * Invokes the given packet event for every registered listener. + * @param event - the packet event to invoke. + */ + public abstract void invokePacketSending(PacketEvent event); + + /** + * Retrieve the associated ID of a packet. + * @param packet - the packet. + * @return The packet ID. + */ + public abstract int getPacketID(Object packet); + + /** + * Associate a given class with the given packet ID. Internal method. + * @param clazz - class to associate. + */ + public abstract void unregisterPacketClass(Class clazz); + + /** + * Register a given class in the packet registry. Internal method. + * @param clazz - class to register. + * @param packetID - the the new associated packet ID. + */ + public abstract void registerPacketClass(Class clazz, int packetID); + + /** + * Retrieves the correct packet class from a given packet ID. + * @param packetID - the packet ID. + * @param forceVanilla - whether or not to look for vanilla classes, not injected classes. + * @return The associated class. + */ + public abstract Class getPacketClassFromID(int packetID, boolean forceVanilla); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java index 33e45f63..bc4cf76f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java @@ -21,12 +21,16 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.error.Report; import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.MapMaker; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.Enhancer; @@ -46,6 +50,9 @@ class InjectedArrayList extends ArrayList { */ private static final long serialVersionUID = -1173865905404280990L; + // Fake inverted proxy objects + private static ConcurrentMap delegateLookup = new MapMaker().weakKeys().makeMap(); + private transient PlayerInjector injector; private transient Set ignoredPackets; private transient ClassLoader classLoader; @@ -108,10 +115,7 @@ class InjectedArrayList extends ArrayList { ListenerInvoker invoker = injector.getInvoker(); int packetID = invoker.getPacketID(source); - Class type = invoker.getPacketClassFromID(packetID, true); - System.out.println(type.getName()); - // We want to subtract the byte amount that were added to the running // total of outstanding packets. Otherwise, cancelling too many packets // might cause a "disconnect.overflow" error. @@ -131,7 +135,7 @@ class InjectedArrayList extends ArrayList { // ect. // } Enhancer ex = new Enhancer(); - ex.setSuperclass(type); + ex.setSuperclass(MinecraftReflection.getPacketClass()); ex.setInterfaces(new Class[] { FakePacket.class } ); ex.setUseCache(true); ex.setClassLoader(classLoader); @@ -143,7 +147,10 @@ class InjectedArrayList extends ArrayList { try { // Temporarily associate the fake packet class invoker.registerPacketClass(proxyClass, packetID); - return proxyClass.newInstance(); + Object proxy = proxyClass.newInstance(); + + InjectedArrayList.registerDelegate(proxy, source); + return proxy; } catch (Exception e) { // Don't pollute the throws tree @@ -154,18 +161,33 @@ class InjectedArrayList extends ArrayList { } } + /** + * Ensure that the inverted integer proxy uses the given object as source. + * @param proxy - inverted integer proxy. + * @param source - source object. + */ + private static void registerDelegate(Object proxy, Object source) { + delegateLookup.put(proxy, source); + } + /** * Inverts the integer result of every integer method. * @author Kristian */ - private class InvertedIntegerCallback implements MethodInterceptor { + private class InvertedIntegerCallback implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + final Object delegate = delegateLookup.get(obj); + + if (delegate == null) { + throw new IllegalStateException("Unable to find delegate source for " + obj); + } + if (method.getReturnType().equals(int.class) && args.length == 0) { - Integer result = (Integer) proxy.invokeSuper(obj, args); + Integer result = (Integer) proxy.invoke(delegate, args); return -result; } else { - return proxy.invokeSuper(obj, args); + return proxy.invoke(delegate, args); } } } From 02b5dec3044a16eab7ac2cd54526236cb6fcda3a Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 13 May 2013 03:49:48 +0200 Subject: [PATCH 44/46] Permit cross edges when validating dependencies. FIXES 91. --- .../protocol/injector/PluginVerifier.java | 24 +- .../injector/player/InjectedArrayList.java | 387 +++++++++--------- 2 files changed, 211 insertions(+), 200 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java index ebb98a26..2e66cf5d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java @@ -83,7 +83,7 @@ class PluginVerifier { * @throws PluginNotFoundException If a plugin with the given name cannot be found. */ private Plugin getPlugin(String pluginName) { - Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName); + Plugin plugin = getPluginOrDefault(pluginName); // Ensure that the plugin exists if (plugin != null) @@ -92,6 +92,15 @@ class PluginVerifier { throw new PluginNotFoundException("Cannot find plugin " + pluginName); } + /** + * Retrieve a plugin by name. + * @param pluginName - the non-null name of the plugin to retrieve. + * @return The retrieved plugin, or NULL if not found. + */ + private Plugin getPluginOrDefault(String pluginName) { + return Bukkit.getPluginManager().getPlugin(pluginName); + } + /** * Performs simple verifications on the given plugin. *

@@ -183,15 +192,15 @@ class PluginVerifier { return Sets.newHashSet(list); } - // Avoid cycles - private boolean hasDependency(Plugin plugin, Plugin dependency, Set checked) { + // Avoid cycles. DFS. + private boolean hasDependency(Plugin plugin, Plugin dependency, Set checking) { Set childNames = Sets.union( safeConversion(plugin.getDescription().getDepend()), safeConversion(plugin.getDescription().getSoftDepend()) ); // Ensure that the same plugin isn't processed twice - if (!checked.add(plugin.getName())) { + if (!checking.add(plugin.getName())) { throw new IllegalStateException("Cycle detected in dependency graph: " + plugin); } // Look for the dependency in the immediate children @@ -201,13 +210,16 @@ class PluginVerifier { // Recurse through their dependencies for (String childName : childNames) { - Plugin childPlugin = getPlugin(childName); + Plugin childPlugin = getPluginOrDefault(childName); - if (hasDependency(childPlugin, dependency, checked)) { + if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) { return true; } } + // Cross edges are permitted + checking.remove(plugin.getName()); + // No dependency found! return false; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java index bc4cf76f..4884dce0 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java @@ -1,194 +1,193 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.injector.player; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; - -import com.comphenix.protocol.Packets; -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.error.Report; -import com.comphenix.protocol.error.ReportType; -import com.comphenix.protocol.injector.ListenerInvoker; -import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.google.common.collect.MapMaker; - -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - -/** - * The array list that notifies when packets are sent by the server. - * - * @author Kristian - */ -class InjectedArrayList extends ArrayList { - public static final ReportType REPORT_CANNOT_REVERT_CANCELLED_PACKET = new ReportType("Reverting cancelled packet failed."); - - /** - * Silly Eclipse. - */ - private static final long serialVersionUID = -1173865905404280990L; - - // Fake inverted proxy objects - private static ConcurrentMap delegateLookup = new MapMaker().weakKeys().makeMap(); - - private transient PlayerInjector injector; - private transient Set ignoredPackets; - private transient ClassLoader classLoader; - - private transient InvertedIntegerCallback callback; - - public InjectedArrayList(ClassLoader classLoader, PlayerInjector injector, Set ignoredPackets) { - this.classLoader = classLoader; - this.injector = injector; - this.ignoredPackets = ignoredPackets; - this.callback = new InvertedIntegerCallback(); - } - - @Override - public boolean add(Object packet) { - - Object result = null; - - // Check for fake packets and ignored packets - if (packet instanceof FakePacket) { - return true; - } else if (ignoredPackets.contains(packet)) { - // Don't send it to the filters - result = ignoredPackets.remove(packet); - } else { - result = injector.handlePacketSending(packet); - } - - // A NULL packet indicate cancelling - try { - if (result != null) { - super.add(result); - } else { - // We'll use the FakePacket marker instead of preventing the filters - injector.sendServerPacket(createNegativePacket(packet), true); - } - - // Collection.add contract - return true; - - } catch (InvocationTargetException e) { - // Prefer to report this to the user, instead of risking sending it to Minecraft - ProtocolLibrary.getErrorReporter().reportDetailed(this, - Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet) - ); - - // Failure - return false; - } - } - - /** - * Used by a hack that reverses the effect of a cancelled packet. Returns a packet - * whereby every int method's return value is inverted (a => -a). - * - * @param source - packet to invert. - * @return The inverted packet. - */ - Object createNegativePacket(Object source) { - ListenerInvoker invoker = injector.getInvoker(); - - int packetID = invoker.getPacketID(source); - - // We want to subtract the byte amount that were added to the running - // total of outstanding packets. Otherwise, cancelling too many packets - // might cause a "disconnect.overflow" error. - // - // We do that by constructing a special packet of the same type that returns - // a negative integer for all zero-parameter integer methods. This includes the - // size() method, which is used by the queue method to count the number of - // bytes to add. - // - // Essentially, we have: - // - // public class NegativePacket extends [a packet] { - // @Override - // public int size() { - // return -super.size(); - // } - // ect. - // } - Enhancer ex = new Enhancer(); - ex.setSuperclass(MinecraftReflection.getPacketClass()); - ex.setInterfaces(new Class[] { FakePacket.class } ); - ex.setUseCache(true); - ex.setClassLoader(classLoader); - ex.setCallbackType(InvertedIntegerCallback.class); - - Class proxyClass = ex.createClass(); - Enhancer.registerCallbacks(proxyClass, new Callback[] { callback }); - - try { - // Temporarily associate the fake packet class - invoker.registerPacketClass(proxyClass, packetID); - Object proxy = proxyClass.newInstance(); - - InjectedArrayList.registerDelegate(proxy, source); - return proxy; - - } catch (Exception e) { - // Don't pollute the throws tree - throw new RuntimeException("Cannot create fake class.", e); - } finally { - // Remove this association - invoker.unregisterPacketClass(proxyClass); - } - } - - /** - * Ensure that the inverted integer proxy uses the given object as source. - * @param proxy - inverted integer proxy. - * @param source - source object. - */ - private static void registerDelegate(Object proxy, Object source) { - delegateLookup.put(proxy, source); - } - - /** - * Inverts the integer result of every integer method. - * @author Kristian - */ - private class InvertedIntegerCallback implements MethodInterceptor { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - final Object delegate = delegateLookup.get(obj); - - if (delegate == null) { - throw new IllegalStateException("Unable to find delegate source for " + obj); - } - - if (method.getReturnType().equals(int.class) && args.length == 0) { - Integer result = (Integer) proxy.invoke(delegate, args); - return -result; - } else { - return proxy.invoke(delegate, args); - } - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.injector.player; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.MapMaker; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +/** + * The array list that notifies when packets are sent by the server. + * + * @author Kristian + */ +class InjectedArrayList extends ArrayList { + public static final ReportType REPORT_CANNOT_REVERT_CANCELLED_PACKET = new ReportType("Reverting cancelled packet failed."); + + /** + * Silly Eclipse. + */ + private static final long serialVersionUID = -1173865905404280990L; + + // Fake inverted proxy objects + private static ConcurrentMap delegateLookup = new MapMaker().weakKeys().makeMap(); + + private transient PlayerInjector injector; + private transient Set ignoredPackets; + private transient ClassLoader classLoader; + + private transient InvertedIntegerCallback callback; + + public InjectedArrayList(ClassLoader classLoader, PlayerInjector injector, Set ignoredPackets) { + this.classLoader = classLoader; + this.injector = injector; + this.ignoredPackets = ignoredPackets; + this.callback = new InvertedIntegerCallback(); + } + + @Override + public boolean add(Object packet) { + + Object result = null; + + // Check for fake packets and ignored packets + if (packet instanceof FakePacket) { + return true; + } else if (ignoredPackets.contains(packet)) { + // Don't send it to the filters + result = ignoredPackets.remove(packet); + } else { + result = injector.handlePacketSending(packet); + } + + // A NULL packet indicate cancelling + try { + if (result != null) { + super.add(result); + } else { + // We'll use the FakePacket marker instead of preventing the filters + injector.sendServerPacket(createNegativePacket(packet), true); + } + + // Collection.add contract + return true; + + } catch (InvocationTargetException e) { + // Prefer to report this to the user, instead of risking sending it to Minecraft + ProtocolLibrary.getErrorReporter().reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet) + ); + + // Failure + return false; + } + } + + /** + * Used by a hack that reverses the effect of a cancelled packet. Returns a packet + * whereby every int method's return value is inverted (a => -a). + * + * @param source - packet to invert. + * @return The inverted packet. + */ + Object createNegativePacket(Object source) { + ListenerInvoker invoker = injector.getInvoker(); + + int packetID = invoker.getPacketID(source); + + // We want to subtract the byte amount that were added to the running + // total of outstanding packets. Otherwise, cancelling too many packets + // might cause a "disconnect.overflow" error. + // + // We do that by constructing a special packet of the same type that returns + // a negative integer for all zero-parameter integer methods. This includes the + // size() method, which is used by the queue method to count the number of + // bytes to add. + // + // Essentially, we have: + // + // public class NegativePacket extends [a packet] { + // @Override + // public int size() { + // return -super.size(); + // } + // ect. + // } + Enhancer ex = new Enhancer(); + ex.setSuperclass(MinecraftReflection.getPacketClass()); + ex.setInterfaces(new Class[] { FakePacket.class } ); + ex.setUseCache(true); + ex.setClassLoader(classLoader); + ex.setCallbackType(InvertedIntegerCallback.class); + + Class proxyClass = ex.createClass(); + Enhancer.registerCallbacks(proxyClass, new Callback[] { callback }); + + try { + // Temporarily associate the fake packet class + invoker.registerPacketClass(proxyClass, packetID); + Object proxy = proxyClass.newInstance(); + + InjectedArrayList.registerDelegate(proxy, source); + return proxy; + + } catch (Exception e) { + // Don't pollute the throws tree + throw new RuntimeException("Cannot create fake class.", e); + } finally { + // Remove this association + invoker.unregisterPacketClass(proxyClass); + } + } + + /** + * Ensure that the inverted integer proxy uses the given object as source. + * @param proxy - inverted integer proxy. + * @param source - source object. + */ + private static void registerDelegate(Object proxy, Object source) { + delegateLookup.put(proxy, source); + } + + /** + * Inverts the integer result of every integer method. + * @author Kristian + */ + private class InvertedIntegerCallback implements MethodInterceptor { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + final Object delegate = delegateLookup.get(obj); + + if (delegate == null) { + throw new IllegalStateException("Unable to find delegate source for " + obj); + } + + if (method.getReturnType().equals(int.class) && args.length == 0) { + Integer result = (Integer) proxy.invoke(delegate, args); + return -result; + } else { + return proxy.invoke(delegate, args); + } + } + } +} From b613e8f01ee9c5f9377703c9cffdcdb404b4e5bc Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 14 May 2013 01:17:03 +0200 Subject: [PATCH 45/46] Bumping to 2.4.3 --- ProtocolLib/pom.xml | 2 +- ProtocolLib/src/main/resources/config.yml | 2 -- ProtocolLib/src/main/resources/plugin.yml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 4adca81e..414e2599 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.4.2-SNAPSHOT + 2.4.3 jar Provides read/write access to the Minecraft protocol. diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index 1de87503..02c7dd23 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -6,8 +6,6 @@ global: # Number of seconds to wait until a new update is downloaded delay: 43200 # 12 hours - # Last update time - last: 0 metrics: true diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 09c2c107..423ece96 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.4.2-SNAPSHOT +version: 2.4.3 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib From 9b8d61b2b0ddaebd411befe09fb6ee4a84ce8ead Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 14 May 2013 01:19:22 +0200 Subject: [PATCH 46/46] Added small settings file in ItemDisguise --- ItemDisguise/.settings/org.eclipse.core.resources.prefs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ItemDisguise/.settings/org.eclipse.core.resources.prefs diff --git a/ItemDisguise/.settings/org.eclipse.core.resources.prefs b/ItemDisguise/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..aa930803 --- /dev/null +++ b/ItemDisguise/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java=cp1252 +encoding/=cp1252