diff --git a/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java index 2df7f485..44ced03b 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java @@ -34,6 +34,8 @@ import com.comphenix.protocol.utility.MinecraftFields; import com.comphenix.protocol.utility.MinecraftMethods; import com.comphenix.protocol.utility.MinecraftProtocolVersion; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.ObjectReconstructor; +import com.comphenix.protocol.wrappers.Pair; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.google.common.base.Preconditions; import io.netty.buffer.ByteBuf; @@ -48,6 +50,7 @@ import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Socket; @@ -55,8 +58,8 @@ import java.net.SocketAddress; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; - /** * Represents a channel injector. * @author Kristian @@ -88,8 +91,23 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { } catch (Exception ex) { throw new RuntimeException("Encountered an error caused by a reload! Please properly restart your server!", ex); } + + Method hiddenClassMethod = null; + try { + if (Float.parseFloat(System.getProperty("java.class.version")) >= 59) { + hiddenClassMethod = Class.class.getMethod("isHidden"); + } + } catch (NoSuchMethodException ignored) { + + } + IS_HIDDEN_CLASS = hiddenClassMethod; } + // Starting in Java 15 (59), the lambdas are hidden classes and we cannot use reflection to update + // the values anymore. Instead, the object will have to be reconstructed. + private static final Map, ObjectReconstructor> RECONSTRUCTORS = new ConcurrentHashMap<>(); + private static final Method IS_HIDDEN_CLASS; + // Saved accessors private Method decodeBuffer; private Method encodeBuffer; @@ -295,18 +313,18 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { @Override protected Callable onMessageScheduled(final Callable callable, FieldAccessor packetAccessor) { - final PacketEvent event = handleScheduled(callable, packetAccessor); + Pair, PacketEvent> handled = handleScheduled(callable, packetAccessor); // Handle cancelled events - if (event != null && event.isCancelled()) + if (handled.getSecond() != null && handled.getSecond().isCancelled()) return null; return () -> { T result; // This field must only be updated in the pipeline thread - currentEvent = event; - result = callable.call(); + currentEvent = handled.getSecond(); + result = handled.getFirst().call(); currentEvent = null; return result; }; @@ -314,20 +332,20 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { @Override protected Runnable onMessageScheduled(final Runnable runnable, FieldAccessor packetAccessor) { - final PacketEvent event = handleScheduled(runnable, packetAccessor); + Pair handled = handleScheduled(runnable, packetAccessor); // Handle cancelled events - if (event != null && event.isCancelled()) + if (handled.getSecond() != null && handled.getSecond().isCancelled()) return null; return () -> { - currentEvent = event; - runnable.run(); + currentEvent = handled.getSecond(); + handled.getFirst().run(); currentEvent = null; }; } - PacketEvent handleScheduled(Object instance, FieldAccessor accessor) { + Pair handleScheduled(T instance, FieldAccessor accessor) { // Let the filters handle this packet Object original = accessor.get(instance); @@ -338,9 +356,9 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { if (marker != null) { PacketEvent result = new PacketEvent(ChannelInjector.class); result.setNetworkMarker(marker); - return result; + return new Pair<>(instance, result); } else { - return BYPASSED_PACKET; + return new Pair<>(instance, BYPASSED_PACKET); } } @@ -350,11 +368,12 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { // Change packet to be scheduled if (original != changed) { - accessor.set(instance, changed); + instance = (T) (isHiddenClass(instance.getClass()) ? + updatePacketMessageReconstruct(instance, changed, accessor) : + updatePacketMessageSetReflection(instance, changed, accessor)); } } - - return event != null ? event : BYPASSED_PACKET; + return new Pair<>(instance, event != null ? event : BYPASSED_PACKET); } }); @@ -363,6 +382,30 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { } } + /** + * Changes the packet in a packet message using a {@link FieldAccessor}. + */ + private static Object updatePacketMessageSetReflection(Object instance, Object newPacket, FieldAccessor accessor) { + accessor.set(instance, newPacket); + return instance; + } + + /** + * Changes the packet in a packet message using a {@link ObjectReconstructor}. + */ + private static Object updatePacketMessageReconstruct(Object instance, Object newPacket, FieldAccessor accessor) { + final ObjectReconstructor objectReconstructor = + RECONSTRUCTORS.computeIfAbsent(instance.getClass(), ObjectReconstructor::new); + + final Object[] values = objectReconstructor.getValues(instance); + final Field[] fields = objectReconstructor.getFields(); + for (int idx = 0; idx < fields.length; ++idx) + if (fields[idx].equals(accessor.getField())) + values[idx] = newPacket; + + return objectReconstructor.reconstruct(values); + } + /** * Determine if the given object is a compressor or decompressor. * @param handler - object to test. @@ -943,4 +986,15 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector { public Channel getChannel() { return originalChannel; } + + private static boolean isHiddenClass(Class clz) { + if (IS_HIDDEN_CLASS == null) { + return false; + } + try { + return (Boolean) IS_HIDDEN_CLASS.invoke(clz); + } catch (Exception e) { + throw new RuntimeException("Failed to determine whether class '" + clz.getName() + "' is hidden or not", e); + } + } } diff --git a/src/main/java/com/comphenix/protocol/utility/ObjectReconstructor.java b/src/main/java/com/comphenix/protocol/utility/ObjectReconstructor.java new file mode 100644 index 00000000..41d57c84 --- /dev/null +++ b/src/main/java/com/comphenix/protocol/utility/ObjectReconstructor.java @@ -0,0 +1,75 @@ +package com.comphenix.protocol.utility; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +/** + * This class can be used to reconstruct objects. + * + * Note that it is limited to classes where both the order and number of member variables matches the order and number + * of arguments for the first constructor. This means that this class is mostly useful for classes generated by lambdas. + * + * @param The type of the object to reconstruct. + * @author Pim + */ +public class ObjectReconstructor { + + private final Class clz; + private final Field[] fields; + private final Constructor ctor; + + public ObjectReconstructor(final Class clz) { + this.clz = clz; + this.fields = clz.getDeclaredFields(); + for (Field field : fields) + field.setAccessible(true); + this.ctor = clz.getDeclaredConstructors()[0]; + this.ctor.setAccessible(true); + } + + /** + * Gets the values of all member variables of the provided instance. + * @param instance The instance for which to get all the member variables. + * @return The values of the member variables from the instance. + */ + public Object[] getValues(final Object instance) { + final Object[] values = new Object[fields.length]; + for (int idx = 0; idx < fields.length; ++idx) + try { + values[idx] = fields[idx].get(instance); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to access field: " + fields[idx].getName() + + " for class: " + clz.getName(), e); + } + return values; + } + + /** + * Gets the fields in the class. + * @return The fields. + */ + public Field[] getFields() { + return fields; + } + + /** + * Creates a new instance of the class using the new values. + * @param values The new values for the member variables of the class. + * @return The new instance. + */ + public T reconstruct(final Object[] values) { + if (values.length != fields.length) + throw new RuntimeException("Mismatched number of arguments for class: " + clz.getName()); + + try { + return (T) ctor.newInstance(values); + } catch (InstantiationException e) { + throw new RuntimeException("Failed to reconstruct object of type: " + clz.getName(), e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to access constructor of type: " + clz.getName(), e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Failed to invoke constructor of type: " + clz.getName(), e); + } + } +}