Add the ability to intercept the write method of packets.

This is done by constructing a proxy around the class after every
event handler has been invoked, intercepting the write method. Each 
PacketOutputHandler registered by the packet event listeners is 
invoked in turn, modifying a byte array of the data that will be 
written to the network stream.

The byte array is initially filled with the serialized version of the 
packet in the packet event.
This commit is contained in:
Kristian S. Stangeland 2013-07-17 03:52:27 +02:00
parent 6fe7fe46f3
commit 051a4eda87
26 changed files with 661 additions and 71 deletions

View File

@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketContainer;
/**
@ -30,7 +31,6 @@ import com.comphenix.protocol.events.PacketContainer;
* @author Kristian
*/
public interface PacketStream {
/**
* Send a packet to the given player.
* @param reciever - the reciever.
@ -49,6 +49,18 @@ public interface PacketStream {
*/
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters)
throws InvocationTargetException;
/**
* Send a packet to the given player.
* @param reciever - the reciever.
* @param packet - packet to send.
* @param marker - the network marker to use.
* @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, NetworkMarker marker, boolean filters)
throws InvocationTargetException;
/**
* Simulate recieving a certain packet from a given player.
@ -70,4 +82,16 @@ public interface PacketStream {
*/
public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters)
throws IllegalAccessException, InvocationTargetException;
/**
* Simulate recieving a certain packet from a given player.
* @param sender - the sender.
* @param packet - the packet that was sent.
* @param marker - the network marker to use.
* @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.
*/
public void recieveClientPacket(Player sender, PacketContainer packet, NetworkMarker marker, boolean filters)
throws IllegalAccessException, InvocationTargetException;
}

View File

@ -381,9 +381,9 @@ public class AsyncMarker implements Serializable, Comparable<AsyncMarker> {
void sendPacket(PacketEvent event) throws IOException {
try {
if (event.isServerPacket()) {
packetStream.sendServerPacket(event.getPlayer(), event.getPacket(), false);
packetStream.sendServerPacket(event.getPlayer(), event.getPacket(), event.getNetworkMarker(), false);
} else {
packetStream.recieveClientPacket(event.getPlayer(), event.getPacket(), false);
packetStream.recieveClientPacket(event.getPlayer(), event.getPacket(), event.getNetworkMarker(), false);
}
transmitted = true;

View File

@ -0,0 +1,13 @@
package com.comphenix.protocol.events;
/**
* Represents additional options a listener may require.
*
* @author Kristian
*/
public enum ListenerOptions {
/**
* Retrieve the serialized client packet as it appears on the network stream.
*/
INTERCEPT_INPUT_BUFFER,
}

View File

@ -17,6 +17,9 @@
package com.comphenix.protocol.events;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import com.comphenix.protocol.injector.GamePhase;
@ -29,7 +32,7 @@ import com.google.common.collect.Sets;
* @author Kristian
*/
public class ListeningWhitelist {
/**
* A whitelist with no packets - indicates that the listener shouldn't observe any packets.
*/
@ -38,6 +41,7 @@ public class ListeningWhitelist {
private ListenerPriority priority;
private Set<Integer> whitelist;
private GamePhase gamePhase;
private Set<ListenerOptions> options = EnumSet.noneOf(ListenerOptions.class);
/**
* Creates a packet whitelist for a given priority with a set of packet IDs.
@ -83,6 +87,19 @@ public class ListeningWhitelist {
this.gamePhase = gamePhase;
}
/**
* Creates a packet whitelist for a given priority with a set of packet IDs and options.
* @param priority - the listener priority.
* @param whitelist - list of packet IDs to observe/enable.
* @param gamePhase - which game phase to receieve notifications on.
*/
public ListeningWhitelist(ListenerPriority priority, Integer[] whitelist, GamePhase gamePhase, ListenerOptions... options) {
this.priority = priority;
this.whitelist = Sets.newHashSet(whitelist);
this.gamePhase = gamePhase;
this.options.addAll(Arrays.asList(options));
}
/**
* Whether or not this whitelist has any enabled packets.
* @return TRUE if there are any packets, FALSE otherwise.
@ -115,9 +132,17 @@ public class ListeningWhitelist {
return gamePhase;
}
/**
* Retrieve every special option associated with this whitelist.
* @return Every special option.
*/
public Set<ListenerOptions> getOptions() {
return Collections.unmodifiableSet(options);
}
@Override
public int hashCode(){
return Objects.hashCode(priority, whitelist, gamePhase);
return Objects.hashCode(priority, whitelist, gamePhase, options);
}
/**
@ -156,7 +181,9 @@ public class ListeningWhitelist {
if(obj instanceof ListeningWhitelist){
final ListeningWhitelist other = (ListeningWhitelist) obj;
return Objects.equal(priority, other.priority)
&& Objects.equal(whitelist, other.whitelist);
&& Objects.equal(whitelist, other.whitelist)
&& Objects.equal(gamePhase, other.gamePhase)
&& Objects.equal(options, other.options);
} else{
return false;
}
@ -169,6 +196,9 @@ public class ListeningWhitelist {
else
return Objects.toStringHelper(this)
.add("priority", priority)
.add("packets", whitelist).toString();
.add("packets", whitelist)
.add("gamephase", gamePhase)
.add("options", options).
toString();
}
}

View File

@ -0,0 +1,133 @@
package com.comphenix.protocol.events;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
import javax.annotation.Nonnull;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;
public class NetworkMarker {
// Custom network handler
private PriorityQueue<PacketOutputHandler> outputHandlers;
// The input buffer
private ByteBuffer inputBuffer;
private ConnectionSide side;
/**
* Construct a new network marker.
* <p>
* The input buffer is only non-null for client-side packets.
* @param side - whether or not this marker belongs to a client or server packet.
* @param inputBuffer - the read serialized packet data.
*/
public NetworkMarker(@Nonnull ConnectionSide side, byte[] inputBuffer) {
this.side = Preconditions.checkNotNull(side, "side cannot be NULL.");
if (inputBuffer != null) {
this.inputBuffer = ByteBuffer.wrap(inputBuffer);
}
}
/**
* Retrieve whether or not this marker belongs to a client or a server side packet.
* @return The side the parent packet belongs to.
*/
public ConnectionSide getSide() {
return side;
}
/**
* Retrieve the serialized packet data (excluding the header) from the network input stream.
* <p>
* The returned buffer is read-only. If the parent event is a server side packet this
* method throws {@link IllegalStateException}.
* <p>
* It returns NULL if the packet was transmitted by a plugin locally.
* @return A byte buffer containing the raw packet data read from the network.
*/
public ByteBuffer getInputBuffer() {
if (side.isForServer())
throw new IllegalStateException("Server-side packets have no input buffer.");
return inputBuffer != null ? inputBuffer.asReadOnlyBuffer() : null;
}
/**
* Enqueue the given output handler for managing how the current packet will be written to the network stream.
* <p>
* Note that output handlers are not serialized, as most consumers will probably implement them using anonymous classes.
* It is not safe to serialize anonymous classes, as their name depend on the order in which they are declared in the parent class.
* <p>
* This can only be invoked on server side packet events.
* @param handler - the handler that will take part in serializing the packet.
* @return TRUE if it was added, FALSE if it has already been added.
*/
public boolean addOutputHandler(@Nonnull PacketOutputHandler handler) {
checkServerSide();
Preconditions.checkNotNull(handler, "handler cannot be NULL.");
// Lazy initialization - it's imperative that we save space and time here
if (outputHandlers == null) {
outputHandlers = new PriorityQueue<PacketOutputHandler>(10, new Comparator<PacketOutputHandler>() {
@Override
public int compare(PacketOutputHandler o1, PacketOutputHandler o2) {
return Ints.compare(o1.getPriority().getSlot(), o2.getPriority().getSlot());
}
});
}
return outputHandlers.add(handler);
}
/**
* Remove a given output handler from the serialization queue.
* <p>
* This can only be invoked on server side packet events.
* @param handler - the handler to remove.
* @return TRUE if the handler was removed, FALSE otherwise.
*/
public boolean removeOutputHandler(@Nonnull PacketOutputHandler handler) {
checkServerSide();
Preconditions.checkNotNull(handler, "handler cannot be NULL.");
if (outputHandlers != null) {
return outputHandlers.remove(handler);
}
return false;
}
/**
* Retrieve every registered output handler in no particular order.
* @return Every registered output handler.
*/
@Nonnull
public Collection<PacketOutputHandler> getOutputHandlers() {
if (outputHandlers != null) {
return outputHandlers;
} else {
return Collections.emptyList();
}
}
/**
* Ensure that the packet event is server side.
*/
private void checkServerSide() {
if (side.isForClient()) {
throw new IllegalStateException("Must be a server side packet.");
}
}
/**
* Determine if the given marker has any output handlers.
* @param marker - the marker to check.
* @return TRUE if it does, FALSE otherwise.
*/
public static boolean hasOutputHandlers(NetworkMarker marker) {
return marker != null && !marker.getOutputHandlers().isEmpty();
}
}

View File

@ -22,11 +22,11 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.WeakReference;
import java.util.EventObject;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import com.comphenix.protocol.async.AsyncMarker;
import com.google.common.base.Preconditions;
public class PacketEvent extends EventObject implements Cancellable {
/**
@ -43,6 +43,9 @@ public class PacketEvent extends EventObject implements Cancellable {
private AsyncMarker asyncMarker;
private boolean asynchronous;
// Network input and output handlers
private NetworkMarker networkMarker;
// Whether or not a packet event is read only
private boolean readOnly;
@ -56,9 +59,14 @@ public class PacketEvent extends EventObject implements Cancellable {
}
private PacketEvent(Object source, PacketContainer packet, Player player, boolean serverPacket) {
this(source, packet, null, player, serverPacket);
}
private PacketEvent(Object source, PacketContainer packet, NetworkMarker marker, Player player, boolean serverPacket) {
super(source);
this.packet = packet;
this.playerReference = new WeakReference<Player>(player);
this.networkMarker = marker;
this.serverPacket = serverPacket;
}
@ -83,6 +91,18 @@ public class PacketEvent extends EventObject implements Cancellable {
return new PacketEvent(source, packet, client, false);
}
/**
* Creates an event representing a client packet transmission.
* @param source - the event source.
* @param packet - the packet.
* @param marker - the network marker.
* @param client - the client that sent the packet.
* @return The event.
*/
public static PacketEvent fromClient(Object source, PacketContainer packet, NetworkMarker marker, Player client) {
return new PacketEvent(source, packet, marker, client, false);
}
/**
* Creates an event representing a server packet transmission.
* @param source - the event source.
@ -94,6 +114,18 @@ public class PacketEvent extends EventObject implements Cancellable {
return new PacketEvent(source, packet, recipient, true);
}
/**
* Creates an event representing a server packet transmission.
* @param source - the event source.
* @param packet - the packet.
* @param marker - the network marker.
* @param recipient - the client that will receieve the packet.
* @return The event.
*/
public static PacketEvent fromServer(Object source, PacketContainer packet, NetworkMarker marker, Player recipient) {
return new PacketEvent(source, packet, marker, recipient, true);
}
/**
* Create an asynchronous packet event from a synchronous event and a async marker.
* @param event - the original synchronous event.
@ -121,7 +153,7 @@ public class PacketEvent extends EventObject implements Cancellable {
throw new IllegalStateException("The packet event is read-only.");
this.packet = packet;
}
/**
* Retrieves the packet ID.
* @return The current packet ID.
@ -137,7 +169,36 @@ public class PacketEvent extends EventObject implements Cancellable {
public boolean isCancelled() {
return cancel;
}
/**
* Retrieve the object responsible for managing the serialized input and output of a packet.
* <p>
* Note that the serialized input data is only available for client-side packets, and the output handlers
* can only be applied to server-side packets.
* @return The network manager.
*/
public NetworkMarker getNetworkMarker() {
if (networkMarker == null) {
if (isServerPacket()) {
networkMarker = new NetworkMarker(
serverPacket ? ConnectionSide.SERVER_SIDE : ConnectionSide.CLIENT_SIDE, null);
} else {
throw new IllegalStateException("Add the option ListenerOptions.INTERCEPT_INPUT_BUFFER to your listener.");
}
}
return networkMarker;
}
/**
* Update the network manager.
* <p>
* This method is internal - do not call.
* @param networkMarker - the new network manager.
*/
public void setNetworkMarker(NetworkMarker networkMarker) {
this.networkMarker = Preconditions.checkNotNull(networkMarker, "marker cannot be NULL");
}
/**
* Sets whether or not the packet should be cancelled. Uncancelling is possible.
* <p>

View File

@ -0,0 +1,35 @@
package com.comphenix.protocol.events;
import org.bukkit.plugin.Plugin;
/**
* Represents a custom packet serializer onto the network stream.
*
* @author Kristian
*/
public interface PacketOutputHandler {
/**
* Retrieve the priority that decides the order each network handler is allowed to manipulate the output buffer.
* <p>
* Higher priority is executed before lower.
* @return The handler priority.
*/
public ListenerPriority getPriority();
/**
* The plugin that owns this output handler.
* @return The owner plugin.
*/
public Plugin getPlugin();
/**
* Invoked when a given packet is to be written to the output stream.
* <p>
* Note that the buffer is initially filled with the output from the default write method. This excludes
* the packet ID header.
* @param event - the packet that will be outputted.
* @param buffer - the data that is currently scheduled to be outputted.
* @return The modified byte array to write. NULL is not permitted.
*/
public byte[] handle(PacketEvent event, byte[] buffer);
}

View File

@ -19,6 +19,7 @@ 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.PacketListener;
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
@ -49,16 +50,19 @@ public class DelayedPacketManager implements ProtocolManager, InternalManager {
private static class QueuedPacket {
private final Player player;
private final PacketContainer packet;
private final NetworkMarker marker;
private final boolean filtered;
private final ConnectionSide side;
public QueuedPacket(Player player, PacketContainer packet, boolean filtered, ConnectionSide side) {
public QueuedPacket(Player player, PacketContainer packet, NetworkMarker marker, boolean filtered, ConnectionSide side) {
this.player = player;
this.packet = packet;
this.marker = marker;
this.filtered = filtered;
this.side = side;
}
/**
* Retrieve the packet that will be transmitted or receieved.
* @return The packet.
@ -83,6 +87,14 @@ public class DelayedPacketManager implements ProtocolManager, InternalManager {
return side;
}
/**
* Retrieve the associated network marker used to serialize packets on the network stream.
* @return The associated marker.
*/
public NetworkMarker getMarker() {
return marker;
}
/**
* Determine if the packet should be intercepted by packet listeners.
* @return TRUE if it should, FALSE otherwise.
@ -162,10 +174,10 @@ public class DelayedPacketManager implements ProtocolManager, InternalManager {
// Attempt to send it now
switch (packet.getSide()) {
case CLIENT_SIDE:
delegate.recieveClientPacket(packet.getPlayer(), packet.getPacket(), packet.isFiltered());
delegate.recieveClientPacket(packet.getPlayer(), packet.getPacket(), packet.getMarker(), packet.isFiltered());
break;
case SERVER_SIDE:
delegate.sendServerPacket(packet.getPlayer(), packet.getPacket(), packet.isFiltered());
delegate.sendServerPacket(packet.getPlayer(), packet.getPacket(), packet.getMarker(), packet.isFiltered());
break;
default:
@ -197,29 +209,39 @@ public class DelayedPacketManager implements ProtocolManager, InternalManager {
@Override
public void sendServerPacket(Player reciever, PacketContainer packet) throws InvocationTargetException {
sendServerPacket(reciever, packet, true);
sendServerPacket(reciever, packet, null, true);
}
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
sendServerPacket(reciever, packet, null, filters);
}
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, NetworkMarker marker, boolean filters) throws InvocationTargetException {
if (delegate != null) {
delegate.sendServerPacket(reciever, packet, filters);
delegate.sendServerPacket(reciever, packet, marker, filters);
} else {
queuedPackets.add(new QueuedPacket(reciever, packet, filters, ConnectionSide.SERVER_SIDE));
queuedPackets.add(new QueuedPacket(reciever, packet, marker, filters, ConnectionSide.SERVER_SIDE));
}
}
@Override
public void recieveClientPacket(Player sender, PacketContainer packet) throws IllegalAccessException, InvocationTargetException {
recieveClientPacket(sender, packet, true);
recieveClientPacket(sender, packet, null, true);
}
@Override
public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException {
recieveClientPacket(sender, packet, null, filters);
}
@Override
public void recieveClientPacket(Player sender, PacketContainer packet, NetworkMarker marker, boolean filters) throws IllegalAccessException, InvocationTargetException {
if (delegate != null) {
delegate.recieveClientPacket(sender, packet, filters);
delegate.recieveClientPacket(sender, packet, marker, filters);
} else {
queuedPackets.add(new QueuedPacket(sender, packet, filters, ConnectionSide.CLIENT_SIDE));
queuedPackets.add(new QueuedPacket(sender, packet, marker, filters, ConnectionSide.CLIENT_SIDE));
}
}

View File

@ -142,7 +142,6 @@ public class PacketConstructor {
* @throws RuntimeException Minecraft threw an exception.
*/
public PacketContainer createPacket(Object... values) throws FieldAccessException {
try {
// Convert types
for (int i = 0; i < values.length; i++) {

View File

@ -550,11 +550,16 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
@Override
public void sendServerPacket(Player reciever, PacketContainer packet) throws InvocationTargetException {
sendServerPacket(reciever, packet, true);
sendServerPacket(reciever, packet, null, true);
}
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
sendServerPacket(reciever, packet, null, true);
}
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, NetworkMarker marker, boolean filters) throws InvocationTargetException {
if (reciever == null)
throw new IllegalArgumentException("reciever cannot be NULL.");
if (packet == null)
@ -562,25 +567,30 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
// We may have to enable player injection indefinitely after this
if (packetCreation.compareAndSet(false, true))
incrementPhases(GamePhase.PLAYING);
// Inform the MONITOR packets
if (!filters) {
PacketEvent event = PacketEvent.fromServer(this, packet, marker, reciever);
sendingListeners.invokePacketSending(
reporter,
PacketEvent.fromServer(this, packet, reciever),
ListenerPriority.MONITOR);
reporter, event, ListenerPriority.MONITOR);
marker = event.getNetworkMarker();
}
playerInjection.sendServerPacket(reciever, packet, filters);
playerInjection.sendServerPacket(reciever, packet, marker, filters);
}
@Override
public void recieveClientPacket(Player sender, PacketContainer packet) throws IllegalAccessException, InvocationTargetException {
recieveClientPacket(sender, packet, true);
recieveClientPacket(sender, packet, null, true);
}
@Override
public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException {
recieveClientPacket(sender, packet, null, true);
}
@Override
public void recieveClientPacket(Player sender, PacketContainer packet, NetworkMarker marker, boolean filters) throws IllegalAccessException, InvocationTargetException {
if (sender == null)
throw new IllegalArgumentException("sender cannot be NULL.");
if (packet == null)
@ -606,7 +616,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
// Let the monitors know though
recievedListeners.invokePacketSending(
reporter,
PacketEvent.fromClient(this, packet, sender),
PacketEvent.fromClient(this, packet, marker, sender),
ListenerPriority.MONITOR);
}

View File

@ -0,0 +1,92 @@
package com.comphenix.protocol.injector.packet;
import java.io.DataOutput;
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.NoOp;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.error.Report;
import com.comphenix.protocol.error.ReportType;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.MethodInfo;
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.comphenix.protocol.utility.MinecraftReflection;
/**
* Retrieve a packet instance that has its write method intercepted.
* @author Kristian
*/
public class InterceptWritePacket {
public static final ReportType REPORT_CANNOT_FIND_WRITE_PACKET_METHOD = new ReportType("Cannot find write packet method in %s.");
public static final ReportType REPORT_CANNOT_CONSTRUCT_WRITE_PROXY = new ReportType("Cannot construct write proxy packet %s.");
/**
* Matches the readPacketData(DataInputStream) method in Packet.
*/
private static FuzzyMethodContract WRITE_PACKET = FuzzyMethodContract.newBuilder().
returnTypeVoid().
parameterDerivedOf(DataOutput.class).
parameterCount(1).
build();
private ClassLoader classLoader;
private ErrorReporter reporter;
private CallbackFilter filter;
private boolean writePacketIntercepted;
public InterceptWritePacket(ClassLoader classLoader, ErrorReporter reporter) {
this.classLoader = classLoader;
this.reporter = reporter;
}
/**
* Construct a new instance of the proxy object.
* @return New instance of proxy.
*/
public Object constructProxy(Object proxyObject, PacketEvent event, NetworkMarker marker) {
// Construct the proxy object
Enhancer ex = new Enhancer();
// Initialize the shared filter
if (filter == null) {
filter = new CallbackFilter() {
@Override
public int accept(Method method) {
// Skip methods defined in Object
if (WRITE_PACKET.isMatch(MethodInfo.fromMethod(method), null)) {
return 1;
} else {
return 0;
}
}
};
}
// Subclass the generic packet class
ex.setSuperclass(MinecraftReflection.getPacketClass());
ex.setCallbackFilter(filter);
ex.setClassLoader(classLoader);
ex.setCallbacks(new Callback[] {
NoOp.INSTANCE,
new WritePacketModifier(reporter, proxyObject, event, marker)
});
Object proxy = ex.create();
if (proxy != null) {
// Check that we found the read method
if (!writePacketIntercepted) {
reporter.reportWarning(this,
Report.newBuilder(REPORT_CANNOT_FIND_WRITE_PACKET_METHOD).
messageParam(MinecraftReflection.getPacketClass()));
}
}
return proxy;
}
}

View File

@ -137,7 +137,7 @@ class ProxyPacketInjector implements PacketInjector {
/**
* Matches the readPacketData(DataInputStream) method in Packet.
*/
private static FuzzyMethodContract readPacket = FuzzyMethodContract.newBuilder().
private static FuzzyMethodContract READ_PACKET = FuzzyMethodContract.newBuilder().
returnTypeVoid().
parameterDerivedOf(DataInput.class).
parameterCount(1).
@ -240,7 +240,7 @@ class ProxyPacketInjector implements PacketInjector {
// Skip methods defined in Object
if (method.getDeclaringClass().equals(Object.class)) {
return 0;
} else if (readPacket.isMatch(MethodInfo.fromMethod(method), null)) {
} else if (READ_PACKET.isMatch(MethodInfo.fromMethod(method), null)) {
readPacketIntercepted = true;
return 1;
} else {

View File

@ -0,0 +1,124 @@
/*
* 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.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.lang.reflect.Method;
import java.util.PriorityQueue;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.error.Report;
import com.comphenix.protocol.error.ReportType;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketOutputHandler;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class WritePacketModifier implements MethodInterceptor {
public static final ReportType REPORT_CANNOT_WRITE_SERVER_PACKET = new ReportType("Cannot write server packet.");
// Report errors
private final ErrorReporter reporter;
// Marker that contains custom writers
private final Object proxyObject;
private final PacketEvent event;
private final NetworkMarker marker;
public WritePacketModifier(ErrorReporter reporter, Object proxyObject, PacketEvent event, NetworkMarker marker) {
this.proxyObject = proxyObject;
this.event = event;
this.marker = marker;
this.reporter = reporter;
}
@Override
public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
PriorityQueue<PacketOutputHandler> handlers = (PriorityQueue<PacketOutputHandler>) marker.getOutputHandlers();
// If every output handler has been removed - ignore everything
if (!handlers.isEmpty()) {
try {
DataOutput output = (DataOutput) args[0];
// First - we need the initial buffer
ByteArrayOutputStream outputBufferStream = new ByteArrayOutputStream();
proxy.invokeSuper(proxyObject, new Object[] { new DataOutputStream(outputBufferStream) });
byte[] outputBuffer = outputBufferStream.toByteArray();
// Let each handler prepare the actual output
while (!handlers.isEmpty()) {
PacketOutputHandler handler = handlers.poll();
try {
byte[] changed = handler.handle(event, outputBuffer);
// Don't break just because a plugin returned NULL
if (changed != null) {
outputBuffer = changed;
} else {
throw new IllegalStateException("Handler cannot return a NULL array.");
}
} catch (Exception e) {
reporter.reportMinimal(handler.getPlugin(), "PacketOutputHandler.handle()", e);
}
}
// Write that output to the network stream
output.write(outputBuffer);
} catch (Throwable e) {
// Minecraft cannot handle this error
reporter.reportDetailed(this,
Report.newBuilder(REPORT_CANNOT_WRITE_SERVER_PACKET).callerParam(args[0]).error(e)
);
}
}
// Default to the super method
return proxy.invokeSuper(proxyObject, args);
}
/**
* Retrieve the proxied Minecraft object.
* @return The proxied object.
*/
public Object getProxyObject() {
return proxyObject;
}
/**
* Retrieve the associated packet event.
* @return The packet event.
*/
public PacketEvent getEvent() {
return event;
}
/**
* Retrieve the network marker that is in use.
* @return The network marker.
*/
public NetworkMarker getMarker() {
return marker;
}
}

View File

@ -86,7 +86,7 @@ class InjectedArrayList extends ArrayList<Object> {
super.add(result);
} else {
// We'll use the FakePacket marker instead of preventing the filters
injector.sendServerPacket(createNegativePacket(packet), true);
injector.sendServerPacket(createNegativePacket(packet), null, true);
}
// Collection.add contract

View File

@ -31,6 +31,7 @@ import com.comphenix.protocol.Packets;
import com.comphenix.protocol.concurrency.IntegerSet;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.events.ListeningWhitelist;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.GamePhase;
import com.comphenix.protocol.injector.ListenerInvoker;
@ -76,14 +77,10 @@ class NetworkFieldInjector extends PlayerInjector {
// Determine if we're listening
private IntegerSet sendingFilters;
// Used to construct proxy objects
private ClassLoader classLoader;
public NetworkFieldInjector(ClassLoader classLoader, ErrorReporter reporter, Player player,
ListenerInvoker manager, IntegerSet sendingFilters) throws IllegalAccessException {
super(reporter, player, manager);
this.classLoader = classLoader;
super(classLoader, reporter, player, manager);
this.sendingFilters = sendingFilters;
}
@ -105,12 +102,15 @@ class NetworkFieldInjector extends PlayerInjector {
}
@Override
public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException {
public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) throws InvocationTargetException {
if (networkManager != null) {
try {
if (!filtered) {
ignoredPackets.add(packet);
}
if (marker != null) {
queuedMarkers.put(packet, marker);
}
// Note that invocation target exception is a wrapper for a checked exception
queueMethod.invoke(networkManager, packet);
@ -146,7 +146,6 @@ class NetworkFieldInjector extends PlayerInjector {
@Override
public void injectManager() {
if (networkManager != null) {
@SuppressWarnings("rawtypes")

View File

@ -35,6 +35,7 @@ import com.comphenix.protocol.Packets;
import com.comphenix.protocol.concurrency.IntegerSet;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.events.ListeningWhitelist;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.GamePhase;
import com.comphenix.protocol.injector.ListenerInvoker;
@ -51,9 +52,6 @@ public class NetworkObjectInjector extends PlayerInjector {
// Determine if we're listening
private IntegerSet sendingFilters;
// 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
@ -79,9 +77,8 @@ public class NetworkObjectInjector extends PlayerInjector {
*/
public NetworkObjectInjector(ClassLoader classLoader, ErrorReporter reporter, Player player,
ListenerInvoker invoker, IntegerSet sendingFilters) throws IllegalAccessException {
super(reporter, player, invoker);
super(classLoader, reporter, player, invoker);
this.sendingFilters = sendingFilters;
this.classLoader = classLoader;
}
@Override
@ -103,11 +100,15 @@ public class NetworkObjectInjector extends PlayerInjector {
}
@Override
public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException {
public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) throws InvocationTargetException {
Object networkDelegate = filtered ? networkManagerRef.getValue() : networkManagerRef.getOldValue();
if (networkDelegate != null) {
try {
if (marker != null) {
queuedMarkers.put(packet, marker);
}
// Note that invocation target exception is a wrapper for a checked exception
queueMethod.invoke(networkDelegate, packet);

View File

@ -30,6 +30,7 @@ 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.NetworkMarker;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.GamePhase;
import com.comphenix.protocol.injector.ListenerInvoker;
@ -64,9 +65,6 @@ class NetworkServerInjector extends PlayerInjector {
// 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;
@ -78,8 +76,7 @@ class NetworkServerInjector extends PlayerInjector {
ListenerInvoker invoker, IntegerSet sendingFilters,
InjectedServerConnection serverInjection) throws IllegalAccessException {
super(reporter, player, invoker);
this.classLoader = classLoader;
super(classLoader, reporter, player, invoker);
this.sendingFilters = sendingFilters;
this.serverInjection = serverInjection;
}
@ -90,11 +87,15 @@ class NetworkServerInjector extends PlayerInjector {
}
@Override
public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException {
public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) throws InvocationTargetException {
Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue();
if (serverDelegate != null) {
try {
if (marker != null) {
queuedMarkers.put(packet, marker);
}
// Note that invocation target exception is a wrapper for a checked exception
MinecraftMethods.getSendPacketMethod().invoke(serverDelegate, packet);
@ -112,7 +113,6 @@ class NetworkServerInjector extends PlayerInjector {
@Override
public void injectManager() {
if (serverHandlerRef == null)
throw new IllegalStateException("Cannot find server handler.");
// Don't inject twice

View File

@ -6,6 +6,7 @@ import java.net.InetSocketAddress;
import java.util.Set;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.GamePhase;
@ -113,10 +114,11 @@ public interface PlayerInjectionHandler {
* Send the given packet to the given reciever.
* @param reciever - the player receiver.
* @param packet - the packet to send.
* @param marker
* @param filters - whether or not to invoke the packet filters.
* @throws InvocationTargetException If an error occured during sending.
*/
public abstract void sendServerPacket(Player reciever, PacketContainer packet, boolean filters)
public abstract void sendServerPacket(Player reciever, PacketContainer packet, NetworkMarker marker, boolean filters)
throws InvocationTargetException;
/**

View File

@ -24,6 +24,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.Map;
import net.sf.cglib.proxy.Factory;
import org.bukkit.entity.Player;
@ -32,6 +33,7 @@ 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.NetworkMarker;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketListener;
@ -39,6 +41,7 @@ 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.packet.InterceptWritePacket;
import com.comphenix.protocol.injector.server.SocketInjector;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
@ -46,6 +49,7 @@ import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.VolatileField;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.google.common.collect.MapMaker;
public abstract class PlayerInjector implements SocketInjector {
// Disconnect method related reports
@ -120,6 +124,13 @@ public abstract class PlayerInjector implements SocketInjector {
// Handle errors
protected ErrorReporter reporter;
// Used to construct proxy objects
protected ClassLoader classLoader;
// Previous markers
protected Map<Object, NetworkMarker> queuedMarkers = new MapMaker().weakKeys().makeMap();
protected InterceptWritePacket writePacketInterceptor;
// Whether or not the injector has been cleaned
private boolean clean;
@ -127,10 +138,14 @@ public abstract class PlayerInjector implements SocketInjector {
boolean updateOnLogin;
Player updatedPlayer;
public PlayerInjector(ErrorReporter reporter, Player player, ListenerInvoker invoker) throws IllegalAccessException {
public PlayerInjector(ClassLoader classLoader, ErrorReporter reporter, Player player, ListenerInvoker invoker) throws IllegalAccessException {
this.classLoader = classLoader;
this.reporter = reporter;
this.player = player;
this.invoker = invoker;
// Intercept the write method
writePacketInterceptor = new InterceptWritePacket(classLoader, reporter);
}
/**
@ -492,11 +507,12 @@ public abstract class PlayerInjector implements SocketInjector {
/**
* Send a packet to the client.
* @param packet - server packet to send.
* @param marker - the network marker.
* @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;
public abstract void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) throws InvocationTargetException;
/**
* Inject a hook to catch packets sent to the current player.
@ -582,9 +598,11 @@ public abstract class PlayerInjector implements SocketInjector {
// Make sure we're listening
if (id != null && hasListener(id)) {
NetworkMarker marker = queuedMarkers.remove(packet);
// A packet has been sent guys!
PacketContainer container = new PacketContainer(id, packet);
PacketEvent event = PacketEvent.fromServer(invoker, container, currentPlayer);
PacketEvent event = PacketEvent.fromServer(invoker, container, marker, currentPlayer);
invoker.invokePacketSending(event);
// Cancelling is pretty simple. Just ignore the packet.
@ -592,7 +610,14 @@ public abstract class PlayerInjector implements SocketInjector {
return null;
// Right, remember to replace the packet again
return event.getPacket().getHandle();
Object result = event.getPacket().getHandle();
marker = event.getNetworkMarker();
// See if we need to proxy the write method
if (result != null && NetworkMarker.hasOutputHandlers(marker)) {
result = writePacketInterceptor.constructProxy(result, event, marker);
}
return result;
}
} catch (Throwable e) {

View File

@ -38,6 +38,7 @@ 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.NetworkMarker;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketListener;
@ -533,12 +534,12 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler {
* @throws InvocationTargetException If an error occured during sending.
*/
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
public void sendServerPacket(Player reciever, PacketContainer packet, NetworkMarker marker, boolean filters) throws InvocationTargetException {
SocketInjector injector = getInjector(reciever);
// Send the packet, or drop it completely
if (injector != null) {
injector.sendServerPacket(packet.getHandle(), filters);
injector.sendServerPacket(packet.getHandle(), marker, filters);
} else {
throw new PlayerLoggedOutException(String.format(
"Unable to send packet %s (%s): Player %s has logged out.",

View File

@ -9,6 +9,8 @@ import java.util.List;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.NetworkMarker;
public class BukkitSocketInjector implements SocketInjector {
private Player player;
@ -41,9 +43,9 @@ public class BukkitSocketInjector implements SocketInjector {
}
@Override
public void sendServerPacket(Object packet, boolean filtered)
public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered)
throws InvocationTargetException {
QueuedSendPacket command = new QueuedSendPacket(packet, filtered);
QueuedSendPacket command = new QueuedSendPacket(packet, marker, filtered);
// Queue until we can find something better
syncronizedQueue.add(command);
@ -65,7 +67,7 @@ public class BukkitSocketInjector implements SocketInjector {
try {
synchronized(syncronizedQueue) {
for (QueuedSendPacket command : syncronizedQueue) {
delegate.sendServerPacket(command.getPacket(), command.isFiltered());
delegate.sendServerPacket(command.getPacket(), command.getMarker(), command.isFiltered());
}
syncronizedQueue.clear();
}

View File

@ -1,18 +1,30 @@
package com.comphenix.protocol.injector.server;
import com.comphenix.protocol.events.NetworkMarker;
/**
* Represents a single send packet command.
* @author Kristian
*/
class QueuedSendPacket {
private final Object packet;
private final NetworkMarker marker;
private final boolean filtered;
public QueuedSendPacket(Object packet, boolean filtered) {
public QueuedSendPacket(Object packet, NetworkMarker marker, boolean filtered) {
this.packet = packet;
this.marker = marker;
this.filtered = filtered;
}
/**
* Retrieve the network marker.
* @return Marker.
*/
public NetworkMarker getMarker() {
return marker;
}
/**
* Retrieve the underlying packet that will be sent.
* @return The underlying packet.

View File

@ -6,6 +6,8 @@ import java.net.SocketAddress;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.NetworkMarker;
/**
* Represents an injector that only gives access to a player's socket.
*
@ -36,10 +38,11 @@ public interface SocketInjector {
/**
* Send a packet to the client.
* @param packet - server packet to send.
* @param marker - the network marker.
* @param filtered - whether or not the packet will be filtered by our listeners.
* @param InvocationTargetException If an error occured when sending the packet.
*/
public abstract void sendServerPacket(Object packet, boolean filtered)
public abstract void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered)
throws InvocationTargetException;
/**

View File

@ -191,7 +191,7 @@ public class TemporaryPlayerFactory {
* @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);
injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), null, false);
return null;
}
}

View File

@ -7,6 +7,7 @@ import java.util.Set;
import org.bukkit.entity.Player;
import com.comphenix.protocol.concurrency.IntegerSet;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.GamePhase;
@ -69,8 +70,8 @@ class DummyPlayerHandler implements PlayerInjectionHandler {
}
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
injector.sendServerPacket(reciever, packet, filters);
public void sendServerPacket(Player reciever, PacketContainer packet, NetworkMarker marker, boolean filters) throws InvocationTargetException {
injector.sendServerPacket(reciever, packet, marker, filters);
}
@Override

View File

@ -24,6 +24,7 @@ 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.NetworkMarker;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.ListenerInvoker;
@ -460,15 +461,15 @@ public class SpigotPacketInjector implements SpigotPacketListener {
}, CLEANUP_DELAY);
}
}
/**
* Invoked when a plugin wants to sent a packet.
* @param reciever - the packet receiver.
* @param packet - the packet to transmit.
* @param marker - the network marker object.
* @param filters - whether or not to invoke the packet listeners.
* @throws InvocationTargetException If anything went wrong.
*/
void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
void sendServerPacket(Player reciever, PacketContainer packet, NetworkMarker marker, boolean filters) throws InvocationTargetException {
NetworkObjectInjector networkObject = getInjector(reciever);
// If TRUE, process this packet like any other
@ -478,7 +479,7 @@ public class SpigotPacketInjector implements SpigotPacketListener {
ignoredPackets.add(packet.getHandle());
if (networkObject != null)
networkObject.sendServerPacket(packet.getHandle(), filters);
networkObject.sendServerPacket(packet.getHandle(), marker, filters);
else
throw new PlayerLoggedOutException("Player " + reciever + " has logged out");
}