Added the ability to intercept serialized input packets on CraftBukkit.

Spigot will be added later.
This commit is contained in:
Kristian S. Stangeland 2013-07-17 06:46:34 +02:00
parent 8f30199a22
commit 6527c0a7f5
10 changed files with 201 additions and 20 deletions

View File

@ -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));
}
}
/**

View File

@ -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;
}
}

View File

@ -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.
* <p>
* The game phase is used to optmize performance. A listener should only choose BOTH or LOGIN if it's absolutely necessary.
* <p>
* 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)

View File

@ -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.

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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) {

View File

@ -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);
}