diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java index 78a23560..acb760ac 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java @@ -97,7 +97,10 @@ public class ListeningWhitelist { this.priority = priority; this.whitelist = Sets.newHashSet(whitelist); this.gamePhase = gamePhase; - this.options.addAll(Arrays.asList(options)); + + if (options != null) { + this.options.addAll(Arrays.asList(options)); + } } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java index fd6b3546..93ed2641 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java @@ -130,4 +130,23 @@ public class NetworkMarker { public static boolean hasOutputHandlers(NetworkMarker marker) { return marker != null && !marker.getOutputHandlers().isEmpty(); } + + /** + * Retrieve the byte buffer stored in the given marker. + * @param marker - the marker. + * @return The byte buffer, or NULL if not found. + */ + public static byte[] getByteBuffer(NetworkMarker marker) { + if (marker != null) { + ByteBuffer buffer = marker.getInputBuffer(); + + if (buffer != null) { + byte[] data = new byte[buffer.remaining()]; + + buffer.get(data, 0, data.length); + return data; + } + } + return null; + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java index 5043ca25..3d053739 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java @@ -30,7 +30,6 @@ import com.comphenix.protocol.injector.GamePhase; * @author Kristian */ public abstract class PacketAdapter implements PacketListener { - protected Plugin plugin; protected ConnectionSide connectionSide; protected ListeningWhitelist receivingWhitelist = ListeningWhitelist.EMPTY_WHITELIST; @@ -95,6 +94,17 @@ public abstract class PacketAdapter implements PacketListener { this(plugin, connectionSide, listenerPriority, GamePhase.PLAYING, packets); } + /** + * Initialize a packet listener for a single connection side. + * @param plugin - the plugin that spawned this listener. + * @param connectionSide - the packet type the listener is looking for. + * @param options - which listener options to use. + * @param packets - the packet IDs the listener is looking for. + */ + public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, ListenerOptions[] options, Integer... packets) { + this(plugin, connectionSide, ListenerPriority.NORMAL, GamePhase.PLAYING, options, packets); + } + /** * Initialize a packet listener for a single connection side. * @param plugin - the plugin that spawned this listener. @@ -117,6 +127,23 @@ public abstract class PacketAdapter implements PacketListener { * @param packets - the packet IDs the listener is looking for. */ public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, ListenerPriority listenerPriority, GamePhase gamePhase, Integer... packets) { + this(plugin, connectionSide, listenerPriority, gamePhase, new ListenerOptions[0], packets); + } + + /** + * Initialize a packet listener for a single connection side. + *

+ * The game phase is used to optmize performance. A listener should only choose BOTH or LOGIN if it's absolutely necessary. + *

+ * Listener options must be specified in order for {@link NetworkMarker#getInputBuffer()} to function correctly. + * @param plugin - the plugin that spawned this listener. + * @param connectionSide - the packet type the listener is looking for. + * @param listenerPriority - the event priority. + * @param gamePhase - which game phase this listener is active under. + * @param options - which listener options to use. + * @param packets - the packet IDs the listener is looking for. + */ + public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, ListenerPriority listenerPriority, GamePhase gamePhase, ListenerOptions[] options, Integer... packets) { if (plugin == null) throw new IllegalArgumentException("plugin cannot be null"); if (connectionSide == null) @@ -127,12 +154,14 @@ public abstract class PacketAdapter implements PacketListener { throw new IllegalArgumentException("gamePhase cannot be NULL"); if (packets == null) throw new IllegalArgumentException("packets cannot be null"); + if (options == null) + throw new IllegalArgumentException("options cannot be null"); // Add whitelists if (connectionSide.isForServer()) - sendingWhitelist = new ListeningWhitelist(listenerPriority, packets, gamePhase); + sendingWhitelist = new ListeningWhitelist(listenerPriority, packets, gamePhase, options); if (connectionSide.isForClient()) - receivingWhitelist = new ListeningWhitelist(listenerPriority, packets, gamePhase); + receivingWhitelist = new ListeningWhitelist(listenerPriority, packets, gamePhase, options); this.plugin = plugin; this.connectionSide = connectionSide; @@ -180,7 +209,6 @@ public abstract class PacketAdapter implements PacketListener { * @return Name of the given plugin. */ public static String getPluginName(Plugin plugin) { - // Try to get the plugin name try { if (plugin == null) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java index 1fd888d5..f2e65038 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java @@ -52,6 +52,13 @@ public interface ListenerInvoker { */ public InterceptWritePacket getInterceptWritePacket(); + /** + * Determine if a given packet requires input buffering. + * @param packetId - the packet to check. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean requireInputBuffer(int packetId); + /** * Associate a given class with the given packet ID. Internal method. * @param clazz - class to associate. 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 aca665f0..998d32cf 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -46,9 +46,11 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import com.comphenix.protocol.AsynchronousManager; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.async.AsyncMarker; +import com.comphenix.protocol.concurrency.IntegerSet; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.Report; import com.comphenix.protocol.error.ReportType; @@ -138,6 +140,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Intercepting write packet methods private InterceptWritePacket interceptWritePacket; + // Whether or not a packet must be input buffered + private volatile IntegerSet inputBufferedPackets = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + // The two listener containers private SortedPacketListenerList recievedListeners; private SortedPacketListenerList sendingListeners; @@ -323,6 +328,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (hasSending || hasReceiving) { // Add listeners and hooks if (hasSending) { + // This doesn't make any sense + if (sending.getOptions().contains(ListenerOptions.INTERCEPT_INPUT_BUFFER)) { + throw new IllegalArgumentException("Sending whitelist cannot require input bufferes to be intercepted."); + } + verifyWhitelist(listener, sending); sendingListeners.addListener(listener, sending); enablePacketFilters(listener, ConnectionSide.SERVER_SIDE, sending.getWhitelist()); @@ -344,9 +354,30 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Inform our injected hooks packetListeners.add(listener); + updateRequireInputBuffers(); } } + /** + * Invoked when we need to update the input buffer set. + */ + private void updateRequireInputBuffers() { + IntegerSet updated = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + + for (PacketListener listener : packetListeners) { + ListeningWhitelist whitelist = listener.getReceivingWhitelist(); + + // We only check the recieving whitelist + if (whitelist.getOptions().contains(ListenerOptions.INTERCEPT_INPUT_BUFFER)) { + for (int id : whitelist.getWhitelist()) { + updated.add(id); + } + } + } + // Update it + this.inputBufferedPackets = updated; + } + /** * Invoked to handle the different game phases of a added listener. * @param phase - listener's game game phase. @@ -437,6 +468,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok disablePacketFilters(ConnectionSide.SERVER_SIDE, sendingRemoved); if (receivingRemoved != null && receivingRemoved.size() > 0) disablePacketFilters(ConnectionSide.CLIENT_SIDE, receivingRemoved); + updateRequireInputBuffers(); } @Override @@ -467,6 +499,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok handlePacket(sendingListeners, event, true); } } + + @Override + public boolean requireInputBuffer(int packetId) { + return inputBufferedPackets.contains(packetId); + } /** * Handle a packet sending or receiving event. @@ -617,7 +654,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok packetInjector.undoCancel(packet.getID(), mcPacket); if (filters) { - PacketEvent event = packetInjector.packetRecieved(packet, sender); + byte[] data = NetworkMarker.getByteBuffer(marker); + PacketEvent event = packetInjector.packetRecieved(packet, sender, data); if (!event.isCancelled()) mcPacket = event.getPacket().getHandle(); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/CaptureInputStream.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/CaptureInputStream.java new file mode 100644 index 00000000..c42e0034 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/CaptureInputStream.java @@ -0,0 +1,64 @@ +package com.comphenix.protocol.injector.packet; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Represents an input stream that stores every read block of bytes in another output stream. + * + * @author Kristian + */ +class CaptureInputStream extends FilterInputStream { + protected OutputStream out; + + public CaptureInputStream(InputStream in, OutputStream out) { + super(in); + this.out = out; + } + + @Override + public int read() throws IOException { + int value = super.read(); + + // Write the byte + if (value >= 0) + out.write(value); + return value; + } + + @Override + public void close() throws IOException { + super.close(); + out.close(); + } + + @Override + public int read(byte[] b) throws IOException { + int count = super.read(b); + + if (count > 0 ) { + out.write(b, 0, count); + } + return count; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int count = super.read(b, off, len); + + if (count > 0 ) { + out.write(b, off, count); + } + return count; + } + + /** + * Retrieve the output stream that recieves all the read information. + * @return The output stream everything is copied to. + */ + public OutputStream getOutputStream() { + return out; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java index 4276212c..2603c2bb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java @@ -51,9 +51,10 @@ public interface PacketInjector { * Let the packet listeners process the given packet. * @param packet - a packet to process. * @param client - the client that sent the packet. + * @param buffered - a buffer containing the data that had to be read in order to construct the packet. * @return The resulting packet event. */ - public abstract PacketEvent packetRecieved(PacketContainer packet, Player client); + public abstract PacketEvent packetRecieved(PacketContainer packet, Player client, byte[] buffered); /** * Perform any necessary cleanup before unloading ProtocolLib. 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 cb7527a7..25b38039 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 @@ -36,6 +36,8 @@ 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.ConnectionSide; +import com.comphenix.protocol.events.NetworkMarker; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.ListenerInvoker; @@ -298,6 +300,15 @@ class ProxyPacketInjector implements PacketInjector { return true; } + /** + * Determine if the data a packet read must be buffered. + * @param packetId - the packet to check. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean requireInputBuffers(int packetId) { + return manager.requireInputBuffer(packetId); + } + @Override public boolean hasPacketHandler(int packetID) { return PacketRegistry.getPreviousPackets().containsKey(packetID); @@ -309,13 +320,13 @@ class ProxyPacketInjector implements PacketInjector { } // Called from the ReadPacketModified monitor - public PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) { + public PacketEvent packetRecieved(PacketContainer packet, DataInputStream input, byte[] buffered) { try { Player client = playerInjection.getPlayerByConnection(input); // Never invoke a event if we don't know where it's from if (client != null) { - return packetRecieved(packet, client); + return packetRecieved(packet, client, buffered); } else { // Hack #2 - Caused by our server socket injector if (packet.getID() != Packets.Client.GET_INFO) @@ -330,15 +341,10 @@ class ProxyPacketInjector implements PacketInjector { } } - /** - * Let the packet listeners process the given packet. - * @param packet - a packet to process. - * @param client - the client that sent the packet. - * @return The resulting packet event. - */ @Override - public PacketEvent packetRecieved(PacketContainer packet, Player client) { - PacketEvent event = PacketEvent.fromClient((Object) manager, packet, client); + public PacketEvent packetRecieved(PacketContainer packet, Player client, byte[] buffered) { + NetworkMarker marker = buffered != null ? new NetworkMarker(ConnectionSide.CLIENT_SIDE, buffered) : null; + PacketEvent event = PacketEvent.fromClient((Object) manager, packet, marker, client); manager.invokePacketRecieving(event); return event; 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 0c8ace3f..0e593397 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 @@ -17,7 +17,9 @@ package com.comphenix.protocol.injector.packet; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; +import java.io.InputStream; import java.lang.reflect.Method; import java.util.Map; @@ -89,6 +91,18 @@ class ReadPacketModifier implements MethodInterceptor { Object overridenObject = override.get(thisObj); Object returnValue = null; + ByteArrayOutputStream bufferStream = null; + + // See if we need to buffer the read data + if (isReadPacketDataMethod && packetInjector.requireInputBuffers(packetID)) { + CaptureInputStream captured = new CaptureInputStream( + (InputStream) args[0], + bufferStream = new ByteArrayOutputStream()); + + // Stash it back + args[0] = new DataInputStream(captured); + } + if (overridenObject != null) { // This packet has been cancelled if (overridenObject == CANCEL_MARKER) { @@ -109,10 +123,11 @@ class ReadPacketModifier implements MethodInterceptor { try { // We need this in order to get the correct player DataInputStream input = (DataInputStream) args[0]; - + byte[] buffer = bufferStream != null ? bufferStream.toByteArray() : null; + // Let the people know PacketContainer container = new PacketContainer(packetID, thisObj); - PacketEvent event = packetInjector.packetRecieved(container, input); + PacketEvent event = packetInjector.packetRecieved(container, input, buffer); // Handle override if (event != null) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java index 2eb32386..783e36ff 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java @@ -51,7 +51,7 @@ class DummyPacketInjector implements PacketInjector { } @Override - public PacketEvent packetRecieved(PacketContainer packet, Player client) { + public PacketEvent packetRecieved(PacketContainer packet, Player client, byte[] buffered) { return injector.packetReceived(packet, client); }