1176 lines
37 KiB
Java
1176 lines
37 KiB
Java
/*
|
|
* 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;
|
|
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import javax.annotation.Nullable;
|
|
|
|
import com.comphenix.protocol.AsynchronousManager;
|
|
import com.comphenix.protocol.PacketType;
|
|
import com.comphenix.protocol.PacketType.Sender;
|
|
import com.comphenix.protocol.async.AsyncFilterManager;
|
|
import com.comphenix.protocol.async.AsyncMarker;
|
|
import com.comphenix.protocol.error.ErrorReporter;
|
|
import com.comphenix.protocol.error.Report;
|
|
import com.comphenix.protocol.error.ReportType;
|
|
import com.comphenix.protocol.events.*;
|
|
import com.comphenix.protocol.injector.netty.ProtocolInjector;
|
|
import com.comphenix.protocol.injector.netty.WirePacket;
|
|
import com.comphenix.protocol.injector.packet.InterceptWritePacket;
|
|
import com.comphenix.protocol.injector.packet.PacketInjector;
|
|
import com.comphenix.protocol.injector.packet.PacketInjectorBuilder;
|
|
import com.comphenix.protocol.injector.packet.PacketRegistry;
|
|
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
|
import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy;
|
|
import com.comphenix.protocol.injector.player.PlayerInjector.ServerHandlerNull;
|
|
import com.comphenix.protocol.injector.player.PlayerInjectorBuilder;
|
|
import com.comphenix.protocol.injector.spigot.SpigotPacketInjector;
|
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
|
import com.comphenix.protocol.utility.MinecraftReflection;
|
|
import com.comphenix.protocol.utility.MinecraftVersion;
|
|
import com.comphenix.protocol.utility.Util;
|
|
import com.google.common.base.Objects;
|
|
import com.google.common.base.Preconditions;
|
|
import com.google.common.base.Predicate;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Sets;
|
|
|
|
import io.netty.channel.Channel;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.Server;
|
|
import org.bukkit.World;
|
|
import org.bukkit.entity.Entity;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.EventPriority;
|
|
import org.bukkit.event.Listener;
|
|
import org.bukkit.event.player.PlayerJoinEvent;
|
|
import org.bukkit.event.player.PlayerLoginEvent;
|
|
import org.bukkit.event.player.PlayerQuitEvent;
|
|
import org.bukkit.event.server.PluginDisableEvent;
|
|
import org.bukkit.plugin.Plugin;
|
|
import org.bukkit.plugin.PluginManager;
|
|
|
|
public final class PacketFilterManager implements ListenerInvoker, InternalManager {
|
|
|
|
public static final ReportType REPORT_CANNOT_LOAD_PACKET_LIST = new ReportType("Cannot load server and client packet list.");
|
|
public static final ReportType REPORT_CANNOT_INITIALIZE_PACKET_INJECTOR = new ReportType("Unable to initialize packet injector");
|
|
|
|
public static final ReportType REPORT_PLUGIN_DEPEND_MISSING =
|
|
new ReportType("%s doesn't depend on ProtocolLib. Check that its plugin.yml has a 'depend' directive.");
|
|
|
|
// Registering packet IDs that are not supported
|
|
public static final ReportType REPORT_UNSUPPORTED_SERVER_PACKET = new ReportType("[%s] Unsupported server packet in current Minecraft version: %s");
|
|
public static final ReportType REPORT_UNSUPPORTED_CLIENT_PACKET = new ReportType("[%s] Unsupported client packet in current Minecraft version: %s");
|
|
|
|
// Problems injecting and uninjecting players
|
|
public static final ReportType REPORT_CANNOT_UNINJECT_PLAYER = new ReportType("Unable to uninject net handler for player.");
|
|
public static final ReportType REPORT_CANNOT_UNINJECT_OFFLINE_PLAYER = new ReportType("Unable to uninject logged off player.");
|
|
public static final ReportType REPORT_CANNOT_INJECT_PLAYER = new ReportType("Unable to inject player.");
|
|
|
|
public static final ReportType REPORT_CANNOT_UNREGISTER_PLUGIN = new ReportType("Unable to handle disabled plugin.");
|
|
public static final ReportType REPORT_PLUGIN_VERIFIER_ERROR = new ReportType("Verifier error: %s");
|
|
|
|
/**
|
|
* The number of ticks in a second.
|
|
*/
|
|
public static final int TICKS_PER_SECOND = 20;
|
|
|
|
// The amount of time to wait until we actually unhook every player
|
|
private static final int UNHOOK_DELAY = 5 * TICKS_PER_SECOND;
|
|
|
|
// Delayed unhook
|
|
private DelayedSingleTask unhookTask;
|
|
|
|
// Create a concurrent set
|
|
private Set<PacketListener> packetListeners =
|
|
Collections.newSetFromMap(new ConcurrentHashMap<PacketListener, Boolean>());
|
|
|
|
// Packet injection
|
|
private PacketInjector packetInjector;
|
|
|
|
// Different injection types per game phase
|
|
private PlayerInjectionHandler playerInjection;
|
|
|
|
// Intercepting write packet methods
|
|
private InterceptWritePacket interceptWritePacket;
|
|
|
|
// Whether or not a packet must be input buffered
|
|
private volatile Set<PacketType> inputBufferedPackets = Sets.newHashSet();
|
|
|
|
// The two listener containers
|
|
private SortedPacketListenerList recievedListeners;
|
|
private SortedPacketListenerList sendingListeners;
|
|
|
|
// Whether or not this class has been closed
|
|
private volatile boolean hasClosed;
|
|
|
|
// The default class loader
|
|
private ClassLoader classLoader;
|
|
|
|
// Error repoter
|
|
private ErrorReporter reporter;
|
|
|
|
// The current server
|
|
private Server server;
|
|
|
|
// The current ProtocolLib library
|
|
private Plugin library;
|
|
|
|
// The async packet handler
|
|
private AsyncFilterManager asyncFilterManager;
|
|
|
|
// Valid server and client packets
|
|
private boolean knowsServerPackets;
|
|
private boolean knowsClientPackets;
|
|
|
|
// Ensure that we're not performing too may injections
|
|
private AtomicInteger phaseLoginCount = new AtomicInteger(0);
|
|
private AtomicInteger phasePlayingCount = new AtomicInteger(0);
|
|
|
|
// Whether or not plugins are using the send/receive methods
|
|
private AtomicBoolean packetCreation = new AtomicBoolean();
|
|
|
|
// Spigot listener, if in use
|
|
private SpigotPacketInjector spigotInjector;
|
|
|
|
// Netty injector (for 1.7.2)
|
|
private ProtocolInjector nettyInjector;
|
|
|
|
// Plugin verifier
|
|
private PluginVerifier pluginVerifier;
|
|
|
|
// Whether or not Location.distance(Location) exists - we assume this is the case
|
|
private boolean hasRecycleDistance = true;
|
|
|
|
// The current Minecraft version
|
|
private MinecraftVersion minecraftVersion;
|
|
|
|
// Login packets
|
|
private LoginPackets loginPackets;
|
|
|
|
// Debug mode
|
|
private boolean debug;
|
|
|
|
/**
|
|
* Only create instances of this class if ProtocolLib is disabled.
|
|
* @param builder - PacketFilterBuilder
|
|
*/
|
|
public PacketFilterManager(PacketFilterBuilder builder) {
|
|
// Used to determine if injection is needed
|
|
Predicate<GamePhase> isInjectionNecessary = new Predicate<GamePhase>() {
|
|
@Override
|
|
public boolean apply(@Nullable GamePhase phase) {
|
|
boolean result = true;
|
|
|
|
if (phase.hasLogin())
|
|
result &= getPhaseLoginCount() > 0;
|
|
// Note that we will still hook players if the unhooking has been delayed
|
|
if (phase.hasPlaying())
|
|
result &= getPhasePlayingCount() > 0 || unhookTask.isRunning();
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Listener containers
|
|
this.recievedListeners = new SortedPacketListenerList();
|
|
this.sendingListeners = new SortedPacketListenerList();
|
|
|
|
// References
|
|
this.unhookTask = builder.getUnhookTask();
|
|
this.server = builder.getServer();
|
|
this.classLoader = builder.getClassLoader();
|
|
this.reporter = builder.getReporter();
|
|
|
|
// The plugin verifier - we don't want to stop ProtocolLib just because its failing
|
|
try {
|
|
this.pluginVerifier = new PluginVerifier(builder.getLibrary());
|
|
} catch (OutOfMemoryError e) {
|
|
throw e;
|
|
} catch (Throwable e) {
|
|
reporter.reportWarning(this, Report.newBuilder(REPORT_PLUGIN_VERIFIER_ERROR).
|
|
messageParam(e.getMessage()).error(e));
|
|
}
|
|
|
|
// Prepare version
|
|
this.minecraftVersion = builder.getMinecraftVersion();
|
|
this.loginPackets = new LoginPackets(minecraftVersion);
|
|
|
|
// The write packet interceptor
|
|
this.interceptWritePacket = new InterceptWritePacket(reporter);
|
|
|
|
// Use the correct injection type
|
|
if (MinecraftReflection.isUsingNetty()) {
|
|
this.nettyInjector = new ProtocolInjector(builder.getLibrary(), this, reporter);
|
|
this.playerInjection = nettyInjector.getPlayerInjector();
|
|
this.packetInjector = nettyInjector.getPacketInjector();
|
|
|
|
} else if (builder.isNettyEnabled()) {
|
|
this.spigotInjector = new SpigotPacketInjector(reporter, this, server);
|
|
this.playerInjection = spigotInjector.getPlayerHandler();
|
|
this.packetInjector = spigotInjector.getPacketInjector();
|
|
|
|
// Set real injector, in case we need it
|
|
spigotInjector.setProxyPacketInjector(PacketInjectorBuilder.newBuilder().
|
|
invoker(this).
|
|
reporter(reporter).
|
|
playerInjection(playerInjection).
|
|
buildInjector()
|
|
);
|
|
|
|
} else {
|
|
// Initialize standard injection mangers
|
|
this.playerInjection = PlayerInjectorBuilder.newBuilder().
|
|
invoker(this).
|
|
server(server).
|
|
reporter(reporter).
|
|
packetListeners(packetListeners).
|
|
injectionFilter(isInjectionNecessary).
|
|
version(builder.getMinecraftVersion()).
|
|
buildHandler();
|
|
|
|
this.packetInjector = PacketInjectorBuilder.newBuilder().
|
|
invoker(this).
|
|
reporter(reporter).
|
|
playerInjection(playerInjection).
|
|
buildInjector();
|
|
}
|
|
this.asyncFilterManager = builder.getAsyncManager();
|
|
this.library = builder.getLibrary();
|
|
|
|
// Attempt to load the list of server and client packets
|
|
try {
|
|
knowsServerPackets = PacketRegistry.getClientPacketTypes() != null;
|
|
knowsClientPackets = PacketRegistry.getServerPacketTypes() != null;
|
|
} catch (FieldAccessException e) {
|
|
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_PACKET_LIST).error(e));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct a new packet filter builder.
|
|
* @return New builder.
|
|
*/
|
|
public static PacketFilterBuilder newBuilder() {
|
|
return new PacketFilterBuilder();
|
|
}
|
|
|
|
@Override
|
|
public int getProtocolVersion(Player player) {
|
|
return playerInjection.getProtocolVersion(player);
|
|
}
|
|
|
|
@Override
|
|
public MinecraftVersion getMinecraftVersion() {
|
|
return minecraftVersion;
|
|
}
|
|
|
|
@Override
|
|
public AsynchronousManager getAsynchronousManager() {
|
|
return asyncFilterManager;
|
|
}
|
|
|
|
@Override
|
|
public boolean isDebug() {
|
|
return debug;
|
|
}
|
|
|
|
@Override
|
|
public void setDebug(boolean debug) {
|
|
this.debug = debug;
|
|
|
|
// Inform components that can handle debug mode
|
|
if (nettyInjector != null) {
|
|
nettyInjector.setDebug(debug);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves how the server packets are read.
|
|
* @return Injection method for reading server packets.
|
|
*/
|
|
@Override
|
|
public PlayerInjectHooks getPlayerHook() {
|
|
return playerInjection.getPlayerHook();
|
|
}
|
|
|
|
/**
|
|
* Sets how the server packets are read.
|
|
* @param playerHook - the new injection method for reading server packets.
|
|
*/
|
|
@Override
|
|
public void setPlayerHook(PlayerInjectHooks playerHook) {
|
|
playerInjection.setPlayerHook(playerHook);
|
|
}
|
|
|
|
@Override
|
|
public ImmutableSet<PacketListener> getPacketListeners() {
|
|
return ImmutableSet.copyOf(packetListeners);
|
|
}
|
|
|
|
@Override
|
|
public InterceptWritePacket getInterceptWritePacket() {
|
|
return interceptWritePacket;
|
|
}
|
|
|
|
/**
|
|
* Warn of common programming mistakes.
|
|
* @param plugin - plugin to check.
|
|
*/
|
|
private void printPluginWarnings(Plugin plugin) {
|
|
if (pluginVerifier == null)
|
|
return;
|
|
|
|
try {
|
|
switch (pluginVerifier.verify(plugin)) {
|
|
case NO_DEPEND:
|
|
reporter.reportWarning(this, Report.newBuilder(REPORT_PLUGIN_DEPEND_MISSING).messageParam(plugin.getName()));
|
|
case VALID:
|
|
// Do nothing
|
|
break;
|
|
}
|
|
} catch (Exception e) {
|
|
reporter.reportWarning(this, Report.newBuilder(REPORT_PLUGIN_VERIFIER_ERROR).messageParam(e.getMessage()));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addPacketListener(PacketListener listener) {
|
|
if (listener == null)
|
|
throw new IllegalArgumentException("listener cannot be NULL.");
|
|
|
|
// A listener can only be added once
|
|
if (packetListeners.contains(listener))
|
|
return;
|
|
ListeningWhitelist sending = listener.getSendingWhitelist();
|
|
ListeningWhitelist receiving = listener.getReceivingWhitelist();
|
|
boolean hasSending = sending != null && sending.isEnabled();
|
|
boolean hasReceiving = receiving != null && receiving.isEnabled();
|
|
|
|
// Check plugin
|
|
if (!(hasSending && sending.getOptions().contains(ListenerOptions.SKIP_PLUGIN_VERIFIER)) &&
|
|
!(hasReceiving && receiving.getOptions().contains(ListenerOptions.SKIP_PLUGIN_VERIFIER))) {
|
|
|
|
printPluginWarnings(listener.getPlugin());
|
|
}
|
|
|
|
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, sending.getTypes());
|
|
|
|
// Make sure this is possible
|
|
playerInjection.checkListener(listener);
|
|
}
|
|
if (hasSending)
|
|
incrementPhases(processPhase(sending));
|
|
|
|
// Handle receivers after senders
|
|
if (hasReceiving) {
|
|
verifyWhitelist(listener, receiving);
|
|
recievedListeners.addListener(listener, receiving);
|
|
enablePacketFilters(listener, receiving.getTypes());
|
|
}
|
|
if (hasReceiving)
|
|
incrementPhases(processPhase(receiving));
|
|
|
|
// Inform our injected hooks
|
|
packetListeners.add(listener);
|
|
updateRequireInputBuffers();
|
|
}
|
|
}
|
|
|
|
private GamePhase processPhase(ListeningWhitelist whitelist) {
|
|
// Determine if this is a login packet, ensuring that gamephase detection is enabled
|
|
if (!whitelist.getGamePhase().hasLogin() &&
|
|
!whitelist.getOptions().contains(ListenerOptions.DISABLE_GAMEPHASE_DETECTION)) {
|
|
|
|
for (PacketType type : whitelist.getTypes()) {
|
|
if (loginPackets.isLoginPacket(type)) {
|
|
return GamePhase.BOTH;
|
|
}
|
|
}
|
|
}
|
|
return whitelist.getGamePhase();
|
|
}
|
|
|
|
/**
|
|
* Invoked when we need to update the input buffer set.
|
|
*/
|
|
private void updateRequireInputBuffers() {
|
|
Set<PacketType> updated = Sets.newHashSet();
|
|
|
|
for (PacketListener listener : packetListeners) {
|
|
ListeningWhitelist whitelist = listener.getReceivingWhitelist();
|
|
|
|
// We only check the recieving whitelist
|
|
if (whitelist.getOptions().contains(ListenerOptions.INTERCEPT_INPUT_BUFFER)) {
|
|
updated.addAll(whitelist.getTypes());
|
|
}
|
|
}
|
|
// Update it
|
|
this.inputBufferedPackets = updated;
|
|
this.packetInjector.inputBuffersChanged(updated);
|
|
}
|
|
|
|
/**
|
|
* Invoked to handle the different game phases of a added listener.
|
|
* @param phase - listener's game game phase.
|
|
*/
|
|
private void incrementPhases(GamePhase phase) {
|
|
if (phase.hasLogin())
|
|
phaseLoginCount.incrementAndGet();
|
|
|
|
// We may have to inject into every current player
|
|
if (phase.hasPlaying()) {
|
|
if (phasePlayingCount.incrementAndGet() == 1) {
|
|
// If we're about to uninitialize every player, cancel that instead
|
|
if (unhookTask.isRunning())
|
|
unhookTask.cancel();
|
|
else
|
|
// Inject our hook into already existing players
|
|
initializePlayers(Util.getOnlinePlayers());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked to handle the different game phases of a removed listener.
|
|
* @param phase - listener's game game phase.
|
|
*/
|
|
private void decrementPhases(GamePhase phase) {
|
|
if (phase.hasLogin())
|
|
phaseLoginCount.decrementAndGet();
|
|
|
|
// We may have to inject into every current player
|
|
if (phase.hasPlaying()) {
|
|
if (phasePlayingCount.decrementAndGet() == 0) {
|
|
// Schedule unhooking in the future
|
|
unhookTask.schedule(UNHOOK_DELAY, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
// Inject our hook into already existing players
|
|
uninitializePlayers(Util.getOnlinePlayers());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if the packet IDs in a whitelist is valid.
|
|
* @param listener - the listener that will be mentioned in the error.
|
|
* @param whitelist - whitelist of packet IDs.
|
|
* @throws IllegalArgumentException If the whitelist is illegal.
|
|
*/
|
|
@Override
|
|
public void verifyWhitelist(PacketListener listener, ListeningWhitelist whitelist) {
|
|
for (PacketType type : whitelist.getTypes()) {
|
|
if (type == null) {
|
|
throw new IllegalArgumentException(String.format("Packet type in in listener %s was NULL.",
|
|
PacketAdapter.getPluginName(listener))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removePacketListener(PacketListener listener) {
|
|
if (listener == null)
|
|
throw new IllegalArgumentException("listener cannot be NULL");
|
|
|
|
List<PacketType> sendingRemoved = null;
|
|
List<PacketType> receivingRemoved = null;
|
|
|
|
ListeningWhitelist sending = listener.getSendingWhitelist();
|
|
ListeningWhitelist receiving = listener.getReceivingWhitelist();
|
|
|
|
// Remove from the overal list of listeners
|
|
if (!packetListeners.remove(listener))
|
|
return;
|
|
|
|
// Remove listeners and phases
|
|
if (sending != null && sending.isEnabled()) {
|
|
sendingRemoved = sendingListeners.removeListener(listener, sending);
|
|
decrementPhases(processPhase(sending));
|
|
}
|
|
if (receiving != null && receiving.isEnabled()) {
|
|
receivingRemoved = recievedListeners.removeListener(listener, receiving);
|
|
decrementPhases(processPhase(receiving));
|
|
}
|
|
|
|
// Remove hooks, if needed
|
|
if (sendingRemoved != null && sendingRemoved.size() > 0)
|
|
disablePacketFilters(ConnectionSide.SERVER_SIDE, sendingRemoved);
|
|
if (receivingRemoved != null && receivingRemoved.size() > 0)
|
|
disablePacketFilters(ConnectionSide.CLIENT_SIDE, receivingRemoved);
|
|
updateRequireInputBuffers();
|
|
}
|
|
|
|
@Override
|
|
public void removePacketListeners(Plugin plugin) {
|
|
// Iterate through every packet listener
|
|
for (PacketListener listener : packetListeners) {
|
|
// Remove the listener
|
|
if (Objects.equal(listener.getPlugin(), plugin)) {
|
|
removePacketListener(listener);
|
|
}
|
|
}
|
|
|
|
// Do the same for the asynchronous events
|
|
asyncFilterManager.unregisterAsyncHandlers(plugin);
|
|
}
|
|
|
|
@Override
|
|
public void invokePacketRecieving(PacketEvent event) {
|
|
if (!hasClosed) {
|
|
handlePacket(recievedListeners, event, false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void invokePacketSending(PacketEvent event) {
|
|
if (!hasClosed) {
|
|
handlePacket(sendingListeners, event, true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean requireInputBuffer(int packetId) {
|
|
return inputBufferedPackets.contains(PacketType.findLegacy(packetId, Sender.CLIENT));
|
|
}
|
|
|
|
/**
|
|
* Handle a packet sending or receiving event.
|
|
* <p>
|
|
* Note that we also handle asynchronous events.
|
|
* @param packetListeners - packet listeners that will receive this event.
|
|
* @param event - the evnet to broadcast.
|
|
*/
|
|
private void handlePacket(SortedPacketListenerList packetListeners, PacketEvent event, boolean sending) {
|
|
// By default, asynchronous packets are queued for processing
|
|
if (asyncFilterManager.hasAsynchronousListeners(event)) {
|
|
event.setAsyncMarker(asyncFilterManager.createAsyncMarker());
|
|
}
|
|
|
|
// Process synchronous events
|
|
if (sending)
|
|
packetListeners.invokePacketSending(reporter, event);
|
|
else
|
|
packetListeners.invokePacketRecieving(reporter, event);
|
|
|
|
// To cancel asynchronous processing, use the async marker
|
|
if (!event.isCancelled() && !hasAsyncCancelled(event.getAsyncMarker())) {
|
|
asyncFilterManager.enqueueSyncPacket(event, event.getAsyncMarker());
|
|
|
|
// The above makes a copy of the event, so it's safe to cancel it
|
|
event.setReadOnly(false);
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
|
|
// NULL marker mean we're dealing with no asynchronous listeners
|
|
private boolean hasAsyncCancelled(AsyncMarker marker) {
|
|
return marker == null || marker.isAsyncCancelled();
|
|
}
|
|
|
|
/**
|
|
* Enables packet events for a given packet ID.
|
|
* <p>
|
|
* Note that all packets are disabled by default.
|
|
*
|
|
* @param listener - the listener that requested to enable these filters.
|
|
* @param packets - the packet id(s).
|
|
*/
|
|
private void enablePacketFilters(PacketListener listener, Iterable<PacketType> packets) {
|
|
// Note the difference between unsupported and valid.
|
|
// Every packet ID between and including 0 - 255 is valid, but only a subset is supported.
|
|
|
|
for (PacketType type : packets) {
|
|
// Only register server packets that are actually supported by Minecraft
|
|
if (type.getSender() == Sender.SERVER) {
|
|
// Note that we may update the packet list here
|
|
if (!knowsServerPackets || PacketRegistry.getServerPacketTypes().contains(type))
|
|
playerInjection.addPacketHandler(type, listener.getSendingWhitelist().getOptions());
|
|
else
|
|
reporter.reportWarning(this,
|
|
Report.newBuilder(REPORT_UNSUPPORTED_SERVER_PACKET).messageParam(PacketAdapter.getPluginName(listener), type)
|
|
);
|
|
}
|
|
|
|
// As above, only for client packets
|
|
if (type.getSender() == Sender.CLIENT && packetInjector != null) {
|
|
if (!knowsClientPackets || PacketRegistry.getClientPacketTypes().contains(type))
|
|
packetInjector.addPacketHandler(type, listener.getReceivingWhitelist().getOptions());
|
|
else
|
|
reporter.reportWarning(this,
|
|
Report.newBuilder(REPORT_UNSUPPORTED_CLIENT_PACKET).messageParam(PacketAdapter.getPluginName(listener), type)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disables packet events from a given packet ID.
|
|
* @param packets - the packet id(s).
|
|
* @param side - which side the event no longer should arrive from.
|
|
*/
|
|
private void disablePacketFilters(ConnectionSide side, Iterable<PacketType> packets) {
|
|
if (side == null)
|
|
throw new IllegalArgumentException("side cannot be NULL.");
|
|
|
|
for (PacketType type : packets) {
|
|
if (side.isForServer())
|
|
playerInjection.removePacketHandler(type);
|
|
if (side.isForClient() && packetInjector != null)
|
|
packetInjector.removePacketHandler(type);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void broadcastServerPacket(PacketContainer packet) {
|
|
Preconditions.checkNotNull(packet, "packet cannot be NULL.");
|
|
broadcastServerPacket(packet, Util.getOnlinePlayers());
|
|
}
|
|
|
|
@Override
|
|
public void broadcastServerPacket(PacketContainer packet, Entity entity, boolean includeTracker) {
|
|
Preconditions.checkNotNull(packet, "packet cannot be NULL.");
|
|
Preconditions.checkNotNull(entity, "entity cannot be NULL.");
|
|
List<Player> trackers = getEntityTrackers(entity);
|
|
|
|
// Only add it if it's a player
|
|
if (includeTracker && entity instanceof Player) {
|
|
trackers.add((Player) entity);
|
|
}
|
|
broadcastServerPacket(packet, trackers);
|
|
}
|
|
|
|
@Override
|
|
public void broadcastServerPacket(PacketContainer packet, Location origin, int maxObserverDistance) {
|
|
try {
|
|
// Square the maximum too
|
|
int maxDistance = maxObserverDistance * maxObserverDistance;
|
|
|
|
World world = origin.getWorld();
|
|
Location recycle = origin.clone();
|
|
|
|
// Only broadcast the packet to nearby players
|
|
for (Player player : Util.getOnlinePlayers()) {
|
|
if (world.equals(player.getWorld()) &&
|
|
getDistanceSquared(origin, recycle, player) <= maxDistance) {
|
|
|
|
sendServerPacket(player, packet);
|
|
}
|
|
}
|
|
|
|
} catch (InvocationTargetException e) {
|
|
throw new FieldAccessException("Unable to send server packet.", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the squared distance between a location and a player.
|
|
* @param origin - the origin location.
|
|
* @param recycle - a location object to be recycled, if supported.
|
|
* @param player - the player.
|
|
* @return The squared distance between the player and the origin,
|
|
*/
|
|
private double getDistanceSquared(Location origin, Location recycle, Player player) {
|
|
if (hasRecycleDistance) {
|
|
try {
|
|
return player.getLocation(recycle).distanceSquared(origin);
|
|
} catch (Error e) {
|
|
// Damn it
|
|
hasRecycleDistance = false;
|
|
}
|
|
}
|
|
|
|
// The fallback method
|
|
return player.getLocation().distanceSquared(origin);
|
|
}
|
|
|
|
/**
|
|
* Broadcast a packet to a given iterable of players.
|
|
* @param packet - the packet to broadcast.
|
|
* @param players - the iterable of players.
|
|
*/
|
|
private void broadcastServerPacket(PacketContainer packet, Iterable<Player> players) {
|
|
try {
|
|
for (Player player : players) {
|
|
sendServerPacket(player, packet);
|
|
}
|
|
} catch (InvocationTargetException e) {
|
|
throw new FieldAccessException("Unable to send server packet.", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void sendServerPacket(Player reciever, PacketContainer packet) throws InvocationTargetException {
|
|
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(final Player receiver, final PacketContainer packet, NetworkMarker marker, final boolean filters) throws InvocationTargetException {
|
|
if (receiver == null)
|
|
throw new IllegalArgumentException("receiver cannot be NULL.");
|
|
if (packet == null)
|
|
throw new IllegalArgumentException("packet cannot be NULL.");
|
|
if (packet.getType().getSender() == Sender.CLIENT)
|
|
throw new IllegalArgumentException("Packet of sender CLIENT cannot be sent to a client.");
|
|
|
|
// We may have to enable player injection indefinitely after this
|
|
if (packetCreation.compareAndSet(false, true))
|
|
incrementPhases(GamePhase.PLAYING);
|
|
|
|
if (!filters) {
|
|
// We may have to delay the packet due to non-asynchronous monitor listeners
|
|
if (!Bukkit.isPrimaryThread() && playerInjection.hasMainThreadListener(packet.getType())) {
|
|
final NetworkMarker copy = marker;
|
|
|
|
server.getScheduler().scheduleSyncDelayedTask(library, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
// Prevent infinite loops
|
|
if (!Bukkit.isPrimaryThread())
|
|
throw new IllegalStateException("Scheduled task was not executed on the main thread!");
|
|
sendServerPacket(receiver, packet, copy, filters);
|
|
} catch (Exception e) {
|
|
reporter.reportMinimal(library, "sendServerPacket-run()", e);
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
PacketEvent event = PacketEvent.fromServer(this, packet, marker, receiver, false);
|
|
sendingListeners.invokePacketSending(reporter, event, ListenerPriority.MONITOR);
|
|
marker = NetworkMarker.getNetworkMarker(event);
|
|
}
|
|
playerInjection.sendServerPacket(receiver, packet, marker, filters);
|
|
}
|
|
|
|
@Override
|
|
public void sendWirePacket(Player receiver, int id, byte[] bytes) throws InvocationTargetException {
|
|
WirePacket packet = new WirePacket(id, bytes);
|
|
sendWirePacket(receiver, packet);
|
|
}
|
|
|
|
@Override
|
|
public void sendWirePacket(Player receiver, WirePacket packet) throws InvocationTargetException {
|
|
Channel channel = playerInjection.getChannel(receiver);
|
|
if (channel == null) {
|
|
throw new InvocationTargetException(new NullPointerException(), "Failed to obtain channel for " + receiver.getName());
|
|
}
|
|
|
|
channel.writeAndFlush(packet);
|
|
}
|
|
|
|
@Override
|
|
public void recieveClientPacket(Player sender, PacketContainer packet) throws IllegalAccessException, InvocationTargetException {
|
|
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 (sender == null)
|
|
throw new IllegalArgumentException("sender cannot be NULL.");
|
|
if (packet == null)
|
|
throw new IllegalArgumentException("packet cannot be NULL.");
|
|
if (packet.getType().getSender() == Sender.SERVER)
|
|
throw new IllegalArgumentException("Packet of sender SERVER cannot be sent to the server.");
|
|
|
|
// And here too
|
|
if (packetCreation.compareAndSet(false, true))
|
|
incrementPhases(GamePhase.PLAYING);
|
|
|
|
Object mcPacket = packet.getHandle();
|
|
boolean cancelled = packetInjector.isCancelled(mcPacket);
|
|
|
|
// Make sure the packet isn't cancelled
|
|
if (cancelled) {
|
|
packetInjector.setCancelled(mcPacket, false);
|
|
}
|
|
|
|
if (filters) {
|
|
byte[] data = NetworkMarker.getByteBuffer(marker);
|
|
PacketEvent event = packetInjector.packetRecieved(packet, sender, data);
|
|
|
|
if (!event.isCancelled())
|
|
mcPacket = event.getPacket().getHandle();
|
|
else
|
|
return;
|
|
|
|
} else {
|
|
// Let the monitors know though
|
|
recievedListeners.invokePacketSending(
|
|
reporter,
|
|
PacketEvent.fromClient(this, packet, marker, sender, false),
|
|
ListenerPriority.MONITOR);
|
|
}
|
|
|
|
playerInjection.recieveClientPacket(sender, mcPacket);
|
|
|
|
// Let it stay cancelled
|
|
if (cancelled) {
|
|
packetInjector.setCancelled(mcPacket, true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public PacketContainer createPacket(int id) {
|
|
return createPacket(PacketType.findLegacy(id), true);
|
|
}
|
|
|
|
@Override
|
|
public PacketContainer createPacket(PacketType type) {
|
|
return createPacket(type, true);
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public PacketContainer createPacket(int id, boolean forceDefaults) {
|
|
return createPacket(PacketType.findLegacy(id), forceDefaults);
|
|
}
|
|
|
|
@Override
|
|
public PacketContainer createPacket(PacketType type, boolean forceDefaults) {
|
|
PacketContainer packet = new PacketContainer(type);
|
|
|
|
// Use any default values if possible
|
|
if (forceDefaults) {
|
|
try {
|
|
packet.getModifier().writeDefaults();
|
|
} catch (FieldAccessException e) {
|
|
throw new RuntimeException("Security exception.", e);
|
|
}
|
|
}
|
|
|
|
return packet;
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public PacketConstructor createPacketConstructor(int id, Object... arguments) {
|
|
return PacketConstructor.DEFAULT.withPacket(id, arguments);
|
|
}
|
|
|
|
@Override
|
|
public PacketConstructor createPacketConstructor(PacketType type, Object... arguments) {
|
|
return PacketConstructor.DEFAULT.withPacket(type, arguments);
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public Set<Integer> getSendingFilters() {
|
|
return PacketRegistry.toLegacy(playerInjection.getSendingFilters());
|
|
}
|
|
|
|
@Override
|
|
public Set<Integer> getReceivingFilters() {
|
|
return PacketRegistry.toLegacy(packetInjector.getPacketHandlers());
|
|
}
|
|
@Override
|
|
public Set<PacketType> getSendingFilterTypes() {
|
|
return Collections.unmodifiableSet(playerInjection.getSendingFilters());
|
|
}
|
|
|
|
@Override
|
|
public Set<PacketType> getReceivingFilterTypes() {
|
|
return Collections.unmodifiableSet(packetInjector.getPacketHandlers());
|
|
}
|
|
|
|
@Override
|
|
public void updateEntity(Entity entity, List<Player> observers) throws FieldAccessException {
|
|
EntityUtilities.updateEntity(entity, observers);
|
|
}
|
|
|
|
@Override
|
|
public Entity getEntityFromID(World container, int id) throws FieldAccessException {
|
|
return EntityUtilities.getEntityFromID(container, id);
|
|
}
|
|
|
|
@Override
|
|
public List<Player> getEntityTrackers(Entity entity) throws FieldAccessException {
|
|
return EntityUtilities.getEntityTrackers(entity);
|
|
}
|
|
|
|
/**
|
|
* Initialize the packet injection for every player.
|
|
* @param players - list of players to inject.
|
|
*/
|
|
public void initializePlayers(List<Player> players) {
|
|
for (Player player : players)
|
|
playerInjection.injectPlayer(player, ConflictStrategy.OVERRIDE);
|
|
}
|
|
|
|
/**
|
|
* Uninitialize the packet injection of every player.
|
|
* @param players - list of players to uninject.
|
|
*/
|
|
public void uninitializePlayers(List<Player> players) {
|
|
for (Player player : players) {
|
|
playerInjection.uninjectPlayer(player);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register this protocol manager on Bukkit.
|
|
*
|
|
* @param manager - Bukkit plugin manager that provides player join/leave events.
|
|
* @param plugin - the parent plugin.
|
|
*/
|
|
@Override
|
|
public void registerEvents(PluginManager manager, final Plugin plugin) {
|
|
if (spigotInjector != null && !spigotInjector.register(plugin))
|
|
throw new IllegalArgumentException("Spigot has already been registered.");
|
|
if (nettyInjector != null)
|
|
nettyInjector.inject();
|
|
|
|
manager.registerEvents(new Listener() {
|
|
|
|
@EventHandler(priority = EventPriority.LOWEST)
|
|
public void onPlayerLogin(PlayerLoginEvent event) {
|
|
PacketFilterManager.this.onPlayerLogin(event);
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.LOWEST)
|
|
public void onPrePlayerJoin(PlayerJoinEvent event) {
|
|
PacketFilterManager.this.onPrePlayerJoin(event);
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
PacketFilterManager.this.onPlayerJoin(event);
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
|
PacketFilterManager.this.onPlayerQuit(event);
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPluginDisabled(PluginDisableEvent event) {
|
|
PacketFilterManager.this.onPluginDisabled(event, plugin);
|
|
}
|
|
|
|
}, plugin);
|
|
}
|
|
|
|
private void onPlayerLogin(PlayerLoginEvent event) {
|
|
playerInjection.updatePlayer(event.getPlayer());
|
|
}
|
|
|
|
private void onPrePlayerJoin(PlayerJoinEvent event) {
|
|
playerInjection.updatePlayer(event.getPlayer());
|
|
}
|
|
|
|
private void onPlayerJoin(PlayerJoinEvent event) {
|
|
try {
|
|
// Let's clean up the other injection first.
|
|
playerInjection.uninjectPlayer(event.getPlayer().getAddress());
|
|
playerInjection.injectPlayer(event.getPlayer(), ConflictStrategy.OVERRIDE);
|
|
} catch (ServerHandlerNull e) {
|
|
// Caused by logged out players, or fake login events in MCPC+/Cauldron. Ignore it.
|
|
} catch (Exception e) {
|
|
reporter.reportDetailed(PacketFilterManager.this,
|
|
Report.newBuilder(REPORT_CANNOT_INJECT_PLAYER).callerParam(event).error(e)
|
|
);
|
|
}
|
|
}
|
|
|
|
private void onPlayerQuit(PlayerQuitEvent event) {
|
|
try {
|
|
Player player = event.getPlayer();
|
|
|
|
asyncFilterManager.removePlayer(player);
|
|
playerInjection.handleDisconnect(player);
|
|
playerInjection.uninjectPlayer(player);
|
|
} catch (Exception e) {
|
|
reporter.reportDetailed(PacketFilterManager.this,
|
|
Report.newBuilder(REPORT_CANNOT_UNINJECT_OFFLINE_PLAYER).callerParam(event).error(e)
|
|
);
|
|
}
|
|
}
|
|
|
|
private void onPluginDisabled(PluginDisableEvent event, Plugin protocolLibrary) {
|
|
try {
|
|
// Clean up in case the plugin forgets
|
|
if (event.getPlugin() != protocolLibrary) {
|
|
removePacketListeners(event.getPlugin());
|
|
}
|
|
} catch (Exception e) {
|
|
reporter.reportDetailed(PacketFilterManager.this,
|
|
Report.newBuilder(REPORT_CANNOT_UNREGISTER_PLUGIN).callerParam(event).error(e)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the number of listeners that expect packets during playing.
|
|
* @return Number of listeners.
|
|
*/
|
|
private int getPhasePlayingCount() {
|
|
return phasePlayingCount.get();
|
|
}
|
|
|
|
/**
|
|
* Retrieve the number of listeners that expect packets during login.
|
|
* @return Number of listeners
|
|
*/
|
|
private int getPhaseLoginCount() {
|
|
return phaseLoginCount.get();
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public int getPacketID(Object packet) {
|
|
return PacketRegistry.getPacketID(packet.getClass());
|
|
}
|
|
|
|
@Override
|
|
public PacketType getPacketType(Object packet) {
|
|
if (packet == null)
|
|
throw new IllegalArgumentException("Packet cannot be NULL.");
|
|
if (!MinecraftReflection.isPacketClass(packet))
|
|
throw new IllegalArgumentException("The given object " + packet + " is not a packet.");
|
|
|
|
PacketType type = PacketRegistry.getPacketType(packet.getClass());
|
|
|
|
if (type != null) {
|
|
return type;
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Unable to find associated packet of " + packet + ": Lookup returned NULL.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public void registerPacketClass(Class<?> clazz, int packetID) {
|
|
PacketRegistry.getPacketToID().put(clazz, packetID);
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public void unregisterPacketClass(Class<?> clazz) {
|
|
PacketRegistry.getPacketToID().remove(clazz);
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
public Class<?> getPacketClassFromID(int packetID, boolean forceVanilla) {
|
|
return PacketRegistry.getPacketClassFromID(packetID, forceVanilla);
|
|
}
|
|
|
|
/**
|
|
* Retrieve every known and supported server packet.
|
|
* @return An immutable set of every known server packet.
|
|
* @throws FieldAccessException If we're unable to retrieve the server packet data from Minecraft.
|
|
*/
|
|
@Deprecated
|
|
public static Set<Integer> getServerPackets() throws FieldAccessException {
|
|
return PacketRegistry.getServerPackets();
|
|
}
|
|
|
|
/**
|
|
* Retrieve every known and supported client packet.
|
|
* @return An immutable set of every known client packet.
|
|
* @throws FieldAccessException If we're unable to retrieve the client packet data from Minecraft.
|
|
*/
|
|
@Deprecated
|
|
public static Set<Integer> getClientPackets() throws FieldAccessException {
|
|
return PacketRegistry.getClientPackets();
|
|
}
|
|
|
|
/**
|
|
* Retrieves the current plugin class loader.
|
|
* @return Class loader.
|
|
*/
|
|
public ClassLoader getClassLoader() {
|
|
return classLoader;
|
|
}
|
|
|
|
@Override
|
|
public boolean isClosed() {
|
|
return hasClosed;
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
// Guard
|
|
if (hasClosed)
|
|
return;
|
|
|
|
// Remove packet handlers
|
|
if (packetInjector != null)
|
|
packetInjector.cleanupAll();
|
|
if (spigotInjector != null)
|
|
spigotInjector.cleanupAll();
|
|
if (nettyInjector != null)
|
|
nettyInjector.close();
|
|
|
|
// Remove server handler
|
|
playerInjection.close();
|
|
hasClosed = true;
|
|
|
|
// Remove listeners
|
|
packetListeners.clear();
|
|
recievedListeners = null;
|
|
sendingListeners = null;
|
|
|
|
// Also cleanup the interceptor for the write packet method
|
|
interceptWritePacket.cleanup();
|
|
|
|
// Clean up async handlers. We have to do this last.
|
|
asyncFilterManager.cleanupAll();
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() throws Throwable {
|
|
close();
|
|
}
|
|
}
|