diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java index 2305cc7f..1b8a7132 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java @@ -53,8 +53,7 @@ public class ProtocolLibrary extends JavaPlugin { @Override public void onLoad() { logger = getLoggerSafely(); - protocolManager = new PacketFilterManager( - getClassLoader(), getServer().getScheduler(), logger); + protocolManager = new PacketFilterManager(getClassLoader(), getServer(), logger); } @Override diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/InjectedServerConnection.java b/ProtocolLib/src/com/comphenix/protocol/injector/InjectedServerConnection.java new file mode 100644 index 00000000..ea2576db --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/InjectedServerConnection.java @@ -0,0 +1,189 @@ +package com.comphenix.protocol.injector; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Server; + +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.VolatileField; + +/** + * 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 Method serverConnectionMethod; + private static Field listField; + + private List listFields; + private List> replacedLists; + + private Server server; + private Logger logger; + private boolean hasAttempted; + private boolean hasSuccess; + + private Object minecraftServer = null; + + public InjectedServerConnection(Logger logger, Server server) { + this.listFields = new ArrayList(); + this.replacedLists = new ArrayList>(); + this.logger = logger; + this.server = server; + } + + public void injectList() { + + // Only execute this method once + if (!hasAttempted) + hasAttempted = true; + else + return; + + if (minecraftServerField == null) + minecraftServerField = FuzzyReflection.fromObject(server, true).getFieldByType(".*MinecraftServer"); + + try { + minecraftServer = FieldUtils.readField(minecraftServerField, server, true); + } catch (IllegalAccessException e1) { + logger.log(Level.WARNING, "Cannot extract minecraft server from Bukkit."); + return; + } + + try { + if (serverConnectionMethod == null) + serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()). + getMethodByParameters("getServerConnection", ".*ServerConnection", new String[] {}); + // We're using Minecraft 1.3.1 + injectServerConnection(); + + } catch (RuntimeException e) { + + // Minecraft 1.2.5 or lower + injectListenerThread(); + } + } + + private void injectListenerThread() { + + try { + + if (listenerThreadField == null) + listenerThreadField = FuzzyReflection.fromClass(minecraftServerField.getType()). + getFieldByType(".*NetworkListenThread"); + } catch (RuntimeException e) { + logger.log(Level.SEVERE, "Cannot find listener thread in MinecraftServer."); + return; + } + + Object listenerThread = null; + + // Attempt to get the thread + try { + listenerThread = listenerThreadField.get(minecraftServer); + } catch (Exception e) { + logger.log(Level.WARNING, "Unable to read the listener thread."); + return; + } + + // Ok, great. Get every list field + List lists = FuzzyReflection.fromClass(listenerThreadField.getType()).getFieldListByType(List.class); + + for (Field list : lists) { + injectIntoList(listenerThread, list); + } + + hasSuccess = true; + } + + private void injectServerConnection() { + + Object serverConnection = null; + + // Careful - we might fail + try { + serverConnection = serverConnectionMethod.invoke(minecraftServer); + } catch (Exception ex) { + logger.log(Level.WARNING, "Unable to retrieve server connection", ex); + return; + } + + if (listField == null) + listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true). + getFieldByType("serverConnection", List.class); + injectIntoList(serverConnection, listField); + hasSuccess = true; + } + + @SuppressWarnings("unchecked") + private void injectIntoList(Object instance, Field field) { + VolatileField listFieldRef = new VolatileField(listField, instance, true); + List list = (List) listFieldRef.getValue(); + + // Careful not to inject twice + if (list instanceof ReplacedArrayList) { + replacedLists.add((ReplacedArrayList) list); + } else { + replacedLists.add(new ReplacedArrayList(list)); + listFieldRef.setValue(replacedLists.get(0)); + listFields.add(listFieldRef); + } + } + + /** + * 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/com/comphenix/protocol/injector/MinecraftRegistry.java b/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java index c66ef956..2e372e1e 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java @@ -49,7 +49,7 @@ class MinecraftRegistry { // Initialize it, if we haven't already if (packetToID == null) { try { - Field packetsField = FuzzyReflection.fromClass(Packet.class, true).getFieldByType("java\\.util\\.Map"); + Field packetsField = FuzzyReflection.fromClass(Packet.class, true).getFieldByType("packetsField", Map.class); packetToID = (Map) FieldUtils.readStaticField(packetsField, true); } catch (IllegalAccessException e) { diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java index ba9d6ee0..fb901a1a 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java @@ -13,8 +13,10 @@ import net.sf.cglib.proxy.MethodProxy; import org.bukkit.entity.Player; import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.instances.CollectionGenerator; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.reflect.instances.ExistingGenerator; @@ -28,9 +30,14 @@ import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; public class NetworkServerInjector extends PlayerInjector { private static Method sendPacketMethod; + + private StructureModifier serverHandlerModifier; + private InjectedServerConnection serverInjection; - public NetworkServerInjector(Player player, PacketFilterManager manager, Set sendingFilters) throws IllegalAccessException { + public NetworkServerInjector(Player player, PacketFilterManager manager, + Set sendingFilters, InjectedServerConnection serverInjection) throws IllegalAccessException { super(player, manager, sendingFilters); + this.serverInjection = serverInjection; } @Override @@ -41,6 +48,8 @@ public class NetworkServerInjector extends PlayerInjector { if (hasInitialized) { if (sendPacketMethod == null) sendPacketMethod = FuzzyReflection.fromObject(serverHandler).getMethodByName("sendPacket.*"); + if (serverHandlerModifier == null) + serverHandlerModifier = new StructureModifier(serverHandler.getClass(), null, false); } } @@ -98,12 +107,8 @@ public class NetworkServerInjector extends PlayerInjector { } } - // Delegate to our underlying class - try { - return method.invoke(serverHandler, args); - } catch (InvocationTargetException e) { - throw e.getCause(); - } + // Call the method directly + return proxy.invokeSuper(obj, args); } }); @@ -114,22 +119,46 @@ public class NetworkServerInjector extends PlayerInjector { CollectionGenerator.INSTANCE); Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass); - + serverInjection.replaceServerHandler(serverHandler, proxyObject); + // Inject it now if (proxyObject != null) { + copyTo(serverHandler, proxyObject); serverHandlerRef.setValue(proxyObject); } else { throw new RuntimeException( "Cannot hook player: Unable to find a valid constructor for the NetServerHandler object."); } } - + + /** + * Copy every field in server handler A to server handler B. + * @param source - fields to copy. + * @param destination - fields to copy to. + */ + private void copyTo(Object source, Object destination) { + StructureModifier modifierSource = serverHandlerModifier.withTarget(source); + StructureModifier modifierDest = serverHandlerModifier.withTarget(destination); + + // Copy every field + try { + for (int i = 0; i < modifierSource.size(); i++) { + modifierDest.write(i, modifierSource.read(i)); + } + } catch (FieldAccessException e) { + throw new RuntimeException("Unable to copy fields from NetServerHandler.", e); + } + } + @Override public void cleanupAll() { - if (serverHandlerRef != null) { + if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) { + copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue()); serverHandlerRef.revertValue(); } + serverInjection.revertServerHandler(serverHandler); + try { if (getNetHandler() != null) { // Restore packet listener diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java index 0288ea1d..6d854ccb 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java @@ -34,6 +34,7 @@ import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; +import org.bukkit.Server; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -44,7 +45,6 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; -import org.bukkit.scheduler.BukkitScheduler; import com.comphenix.protocol.AsynchronousManager; import com.comphenix.protocol.ProtocolManager; @@ -92,11 +92,14 @@ public final class PacketFilterManager implements ProtocolManager { private Map playerInjection = new HashMap(); // Player injection type - private PlayerInjectHooks playerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; + private PlayerInjectHooks playerHook = PlayerInjectHooks.NETWORK_HANDLER_FIELDS; // Packet injection private PacketInjector packetInjector; + // Server connection injection + private InjectedServerConnection serverInjection; + // Enabled packet filters private Set sendingFilters = Collections.newSetFromMap(new ConcurrentHashMap()); @@ -122,7 +125,7 @@ public final class PacketFilterManager implements ProtocolManager { /** * Only create instances of this class if protocol lib is disabled. */ - public PacketFilterManager(ClassLoader classLoader, BukkitScheduler scheduler, Logger logger) { + public PacketFilterManager(ClassLoader classLoader, Server server, Logger logger) { if (logger == null) throw new IllegalArgumentException("logger cannot be NULL."); if (classLoader == null) @@ -133,7 +136,8 @@ public final class PacketFilterManager implements ProtocolManager { this.classLoader = classLoader; this.logger = logger; this.packetInjector = new PacketInjector(classLoader, this, connectionLookup); - this.asyncFilterManager = new AsyncFilterManager(logger, scheduler, this); + this.asyncFilterManager = new AsyncFilterManager(logger, server.getScheduler(), this); + this.serverInjection = new InjectedServerConnection(logger, server); } catch (IllegalAccessException e) { logger.log(Level.SEVERE, "Unable to initialize packet injector.", e); } @@ -474,7 +478,7 @@ public final class PacketFilterManager implements ProtocolManager { case NETWORK_MANAGER_OBJECT: return new NetworkObjectInjector(player, this, sendingFilters); case NETWORK_SERVER_OBJECT: - return new NetworkServerInjector(player, this, sendingFilters); + return new NetworkServerInjector(player, this, sendingFilters, serverInjection); default: throw new IllegalArgumentException("Cannot construct a player injector."); } @@ -714,6 +718,8 @@ public final class PacketFilterManager implements ProtocolManager { if (packetInjector != null) packetInjector.cleanupAll(); + // Remove server handler + serverInjection.cleanupAll(); hasClosed = true; // Remove listeners diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java index b92fd3c0..c9c6e3a5 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java @@ -221,6 +221,7 @@ abstract class PlayerInjector { Packet handlePacketRecieved(Packet packet) { // Get the packet ID too Integer id = MinecraftRegistry.getPacketToID().get(packet.getClass()); + System.out.println(id); // Make sure we're listening if (id != null && sendingFilters.contains(id)) { diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/ReplacedArrayList.java b/ProtocolLib/src/com/comphenix/protocol/injector/ReplacedArrayList.java new file mode 100644 index 00000000..64e3b0de --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/ReplacedArrayList.java @@ -0,0 +1,136 @@ +package com.comphenix.protocol.injector; + +import java.util.Collection; +import java.util.List; + +import com.google.common.base.Objects; +import com.google.common.collect.BiMap; +import com.google.common.collect.ForwardingList; +import com.google.common.collect.HashBiMap; + +/** + * Represents an array list that wraps another list, while automatically replacing one element with another. + *

+ * The replaced elements can be recovered. + * + * @author Kristian + * @param - type of the elements we're replacing. + */ +class ReplacedArrayList extends ForwardingList { + private BiMap replaceMap = HashBiMap.create(); + private List underlyingList; + + public ReplacedArrayList(List underlyingList) { + this.underlyingList = underlyingList; + } + + @Override + public boolean add(TKey element) { + if (replaceMap.containsKey(element)) { + return super.add(replaceMap.get(element)); + } else { + return super.add(element); + } + } + + @Override + public void add(int index, TKey element) { + if (replaceMap.containsKey(element)) { + super.add(index, replaceMap.get(element)); + } else { + super.add(index, element); + } + } + + @Override + public boolean addAll(Collection collection) { + int oldSize = size(); + + for (TKey element : collection) + add(element); + return size() != oldSize; + } + + @Override + public boolean addAll(int index, Collection elements) { + int oldSize = size(); + + for (TKey element : elements) + add(index++, element); + return size() != oldSize; + } + + @Override + protected List delegate() { + return underlyingList; + } + + /** + * Add a replace rule. + *

+ * This automatically replaces every existing element. + * @param target - instance to find. + * @param replacement - instance to replace with. + */ + public synchronized void addMapping(TKey target, TKey replacement) { + replaceMap.put(target, replacement); + + // Replace existing elements + replaceAll(target, replacement); + } + + /** + * Revert the given mapping. + * @param target - the instance we replaced. + */ + public synchronized void removeMapping(TKey target) { + // Make sure the mapping exist + if (replaceMap.containsKey(target)) { + TKey replacement = replaceMap.get(target); + replaceMap.remove(target); + + // Revert existing elements + replaceAll(replacement, target); + } + } + + /** + * Replace all instances of the given object. + * @param find - object to find. + * @param replace - object to replace it with. + */ + public synchronized void replaceAll(TKey find, TKey replace) { + for (int i = 0; i < underlyingList.size(); i++) { + if (Objects.equal(underlyingList.get(i), find)) + underlyingList.set(i, replace); + } + } + + /** + * Undo all replacements. + */ + public synchronized void revertAll() { + + // No need to do anything else + if (replaceMap.size() < 1) + return; + + BiMap inverse = replaceMap.inverse(); + + for (int i = 0; i < underlyingList.size(); i++) { + TKey replaced = underlyingList.get(i); + + if (inverse.containsKey(replaced)) { + underlyingList.set(i, inverse.get(replaced)); + } + } + + replaceMap.clear(); + } + + @Override + protected void finalize() throws Throwable { + revertAll(); + super.finalize(); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java index ae30b604..448507f7 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -142,11 +142,38 @@ public class FuzzyReflection { * @return The first method that satisfies the parameter types. */ public Method getMethodByParameters(String name, Class returnType, Class[] args) { + // Find the correct method to call + List methods = getMethodListByParameters(returnType, args); + + if (methods.size() > 0) { + return methods.get(0); + } else { + // That sucks + throw new RuntimeException("Unable to find " + name + " in " + source.getName()); + } + } + /** + * Retrieves a method by looking at the parameter types and return type only. + * @param name - potential name of the method. Only used by the error mechanism. + * @param returnType - regular expression matching the return type of the method to find. + * @param args - regular expressions of the matching parameter types. + * @return The first method that satisfies the parameter types. + */ + public Method getMethodByParameters(String name, String returnTypeRegex, String[] argsRegex) { + + Pattern match = Pattern.compile(returnTypeRegex); + Pattern[] argMatch = new Pattern[argsRegex.length]; + + for (int i = 0; i < argsRegex.length; i++) { + argMatch[i] = Pattern.compile(argsRegex[i]); + } + // Find the correct method to call for (Method method : getMethods()) { - if (method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), args)) { - return method; + if (match.matcher(method.getReturnType().getName()).matches()) { + if (matchParameters(argMatch, method.getParameterTypes())) + return method; } } @@ -154,6 +181,19 @@ public class FuzzyReflection { throw new RuntimeException("Unable to find " + name + " in " + source.getName()); } + private boolean matchParameters(Pattern[] parameterMatchers, Class[] argTypes) { + if (parameterMatchers.length != argTypes.length) + throw new IllegalArgumentException("Arrays must have the same cardinality."); + + // Check types against the regular expressions + for (int i = 0; i < argTypes.length; i++) { + if (!parameterMatchers[i].matcher(argTypes[i].getName()).matches()) + return false; + } + + return true; + } + /** * Retrieves every method that has the given parameter types and return type. * @param returnType - return type of the method to find. @@ -195,6 +235,46 @@ public class FuzzyReflection { nameRegex + " in " + source.getName()); } + /** + * Retrieves the first field with a type equal to or more specific to the given type. + * @param name - name the field probably is given. This will only be used in the error message. + * @param type - type of the field to find. + * @return The first field with a type that is an instance of the given type. + */ + public Field getFieldByType(String name, Class type) { + + List fields = getFieldListByType(type); + + if (fields.size() > 0) { + return fields.get(0); + } else { + // Looks like we're outdated. Too bad. + throw new RuntimeException(String.format("Unable to find a field %s with the type %s in %s", + name, type.getName(), source.getName()) + ); + } + } + + /** + * Retrieves every field with a type equal to or more specific to the given type. + * @param type - type of the fields to find. + * @return Every field with a type that is an instance of the given type. + */ + public List getFieldListByType(Class type) { + + List fields = new ArrayList(); + + // Field with a compatible type + for (Field field : getFields()) { + // A assignable from B -> B instanceOf A + if (type.isAssignableFrom(field.getType())) { + fields.add(field); + } + } + + return fields; + } + /** * Retrieves a field by type. *

diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java b/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java index 4e22f7ef..64272b96 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java @@ -155,6 +155,13 @@ public class VolatileField { } } + /** + * Determine whether or not we'll need to revert the value. + */ + public boolean isCurrentSet() { + return currentSet; + } + private void ensureLoaded() { // Load the value if we haven't already if (!previousLoaded) {